This week Dave Rook (AKA Security Ninja released a new security review tool; Agnitio. In the interest of full disclosure for this review, Dave is a friend and mentor through the Infosec Mentors program. I have been using Agnitio for a couple of weeks now and I have found it to be an interesting and effective tool in the code review process as well as a convenient information repository. Code reviews are often performed in an inconsistent way, depending heavily on the reviewer; Agnitio provides a framework for adding more consistency to these reviews.
Agnitio is a fairly simple tool with a lot of depth. You begin by creating a profile(shown below), which contains basic information about your application including languages used, data sensitivity classification and stored information types. After saving an application profile, you can move onto a security code review. This is the real meat of Agnitio.
Agnitio presents a 66 question checklist covering the nine principals of secure development. With so much application security information typically focused on vulnerabilities, it's refreshing to see clearly broken down security action items for developers. You can answer a review question with "Yes", "No", or "N/A". Answering N/A to a question requires an explanation. I would argue that no could require this as well.
After all of the questions have been answered, you can save your review and export pretty reports. I can't help thinking how helpful these reports could have been in the past when I've inherited applications with little to no security documentation. Agnitio can serve several different purposes depending on the user's job function. As a developer, a centralized, detailed security checklist is helpful in all phases of creating the application. Future versions will include customizable checklists which will allow for more project/company specific code reviews.
Agnitio is also a learning resource. Reading through the code review checklist serves as a more detailed breakdown of secure coding principles. Most developers know their software needs secure communications, for example, but the specific implementation is not always obvious. Agnitio lets a developer know what they need to be thinking about and lays a groundwork for sensible application security related communication between developers.
Overall, I have enjoyed the opportunity to play with Agnitio these past few weeks. I'm excited to see what lies ahead for Agnitio. A few of the features I've heard about have the potential to be very cool. I would recommend Agnitio to anyone with an interest in application security and writing stronger code.
Saturday, November 20, 2010
Wednesday, September 15, 2010
What is your mobile device saying about you in public?
Anyone who attended Defcon this year probably noticed the huge number of Twitter accounts posted on the Wall of Sheep. A lot of people weren't aware that their chosen mobile Twitter client did not use SSL to log in. With the proliferation of wifi enabled mobile devices, it's easier than ever to sniff mobile traffic. That traffic is a goldmine, and many users don't realize what they're sending in clear text.
There are a few benefits to being able to sniff mobile app traffic. Its easier to track where your personal information is ending up. For example, if you are using an app that you're trusting with information from the device, it's often pretty easy to see that information going out in real time to third parties you may not trust as much. I would love to see better control from the device about what urls an app is allowed to send information to; maybe a NoScript style whitelist. If you take a look at some mobile traffic from your device of choice, you may be amazed at the information leakage happening.
Taking a look at your device traffic can also serve as a quick audit of the application. If you can easily see logins going out in clear text, information about you or your device going out to third parties, or hitting a non-relevant URL in the middle, that may be worth taking note of. If an app sends your login to the dev company's server before forwarding it on to the service you're trying to use, that's worth knowing. There are mobile applications that do this sort of thing all the time and being able to see the traffic allows you to decide if this is ok or not.
The benefits of having so much mobile traffic sent in clear text obviously disappear when you're hooked up to public wifi and it's someone else examining the traffic. As the all-in-good-fun Defcon example shows, you could be leaking logins and personal information to anyone who cares to look. It can be easy to take security for granted when you're using a mobile application. Average users have been trained to check for secure connections in their browser before they send sensitive information. But when that visual confirmation is not readily available that check is easy to forget. Unfortunately, apps properly protecting user information in transit are not nearly common enough to take such things for granted.
The problems in mobile application traffic are much the same as the rest of web application security. Although with the number of wifi mobile device users increasing, it will continue to become a bigger issue. Solutions can come from device manufacturers and developers. By considering the information their applications are sending, and encrypting it accordingly, developers can make their app users much safer. Third party API builders also need to consider encryption options or requirements. SSL is not even an option on many APIs.
Manufacturers can improve by providing users more granular control over what their applications are sending and how they are sending it. Several platforms I examined have all made some attempt at this but I have yet to see anyone make it intuitive for the average user or provide complete controls. A visual in-app secure connection indicator would also give users control they currently enjoy on their home computers.
So what do you think? How can we improve the state of traffic encryption on mobile devices? And why arent more developers encrypting sensitive information by default? As always, feedback is much appreciated. You can comment here, email me at jackwillksecurity at gmail or reach me on Twitter @jackwillk
There are a few benefits to being able to sniff mobile app traffic. Its easier to track where your personal information is ending up. For example, if you are using an app that you're trusting with information from the device, it's often pretty easy to see that information going out in real time to third parties you may not trust as much. I would love to see better control from the device about what urls an app is allowed to send information to; maybe a NoScript style whitelist. If you take a look at some mobile traffic from your device of choice, you may be amazed at the information leakage happening.
Taking a look at your device traffic can also serve as a quick audit of the application. If you can easily see logins going out in clear text, information about you or your device going out to third parties, or hitting a non-relevant URL in the middle, that may be worth taking note of. If an app sends your login to the dev company's server before forwarding it on to the service you're trying to use, that's worth knowing. There are mobile applications that do this sort of thing all the time and being able to see the traffic allows you to decide if this is ok or not.
The benefits of having so much mobile traffic sent in clear text obviously disappear when you're hooked up to public wifi and it's someone else examining the traffic. As the all-in-good-fun Defcon example shows, you could be leaking logins and personal information to anyone who cares to look. It can be easy to take security for granted when you're using a mobile application. Average users have been trained to check for secure connections in their browser before they send sensitive information. But when that visual confirmation is not readily available that check is easy to forget. Unfortunately, apps properly protecting user information in transit are not nearly common enough to take such things for granted.
The problems in mobile application traffic are much the same as the rest of web application security. Although with the number of wifi mobile device users increasing, it will continue to become a bigger issue. Solutions can come from device manufacturers and developers. By considering the information their applications are sending, and encrypting it accordingly, developers can make their app users much safer. Third party API builders also need to consider encryption options or requirements. SSL is not even an option on many APIs.
Manufacturers can improve by providing users more granular control over what their applications are sending and how they are sending it. Several platforms I examined have all made some attempt at this but I have yet to see anyone make it intuitive for the average user or provide complete controls. A visual in-app secure connection indicator would also give users control they currently enjoy on their home computers.
So what do you think? How can we improve the state of traffic encryption on mobile devices? And why arent more developers encrypting sensitive information by default? As always, feedback is much appreciated. You can comment here, email me at jackwillksecurity at gmail or reach me on Twitter @jackwillk
Monday, August 16, 2010
Using the OWASP ESAPI PHP - Part 4
It's been a crazy few weeks! I moved across the country, attended some of Security B-sides and all of Defcon; more on the conferences to come later. Because it's been a while between posts, and this is the final post in this series on ESAPI PHP, let's start out by reviewing what we've done so far.
Part 1: We talked a little bit about the ESAPI PHP project, set up a plan for this series and wrote an extremely insecure basic blogging PHP web application.
Part 2: We set up the PHP ESAPI project and started validating user input.
Part 3: We started properly handling our output and used prepared statements to make our database queries safer.
And this week (our final entry in the series), we're going to go over securing our user authentication and sessions. There are some other great controls in ESAPI PHP, but the project is not as mature as the Java version and some of the controls are not quite ready for prime time. So I'm mostly going to stick with what can be implemented now with minimal changes. You can find the files from this week here.
Handling users and sessions
Let's start out listing what we'll be doing to harden our User class. This is not a comprehensive list on hardening a User control, but it includes items that will mostly touch on ESAPI controls in some way.
That sounds like a lot, and it is. From the last post to this one, the User class has more than doubled in size. Luckily, none of it is too difficult. ESAPI PHP does have some objects and utilities for handling users. For the most part though, they're not complete. One thing it does have is very good utilities for handling CSRF tokens via the HTTPUtilties control. We're going to use that while we work on our User class.
We'll start out like we start out every other Class, by including the ESAPI controls we'll require. We require BlogHTTPUtilities because we're going to modify the DefaultHTTPUtilities just a little bit.
We're also going to add a few new properties to our object:
$esapi is our ESAPI object - we've seen this plenty of places before. We store the HTTPUtilities control in $httputils. Since we only have one level of user, we'll just use a boolean called $logged_in to store that information. $salt is used to improve our password hashing. And finally, $expire_time is the number of seconds of inactivity before a user's session expires.
One thing, we'll be storing the salt in the database, so let's alter our user table right now. Also, we'll be hashing the password so we need to make the password field a bit bigger.
We'll just go function by function and go over what we're changing in the User class. First up is our constructor:
Not much we haven't seen before. We create our ESAPI controls and default them to not logged in. Then, we check to see if a user session exists and contains valid information. If so, we change logged_in to true. The retrieve_user function is new, and we'll go over that soon.
Next, we create a few simple get/set methods for our new properties:
The only thing that may not be obvious here is the get_token function. The HTTPUtilities controls handles most everything we need for CSRF tokens, so our getter function just calls HTTPUtilities'.
Now we get to the interesting stuff, starting with our login function:
The first thing you might notice is that if we're given a valid username, we're selecting all of the user's information from the database. This is because of the addition of the salt field. We need to hash the password the user submitted to us to compare the two. We might as well grab all the information in one fell swoop rather than making two database calls.
The next thing we'll noticed we changed a bit is the num_rows check. We know that this query should never return more than one row, so we should check for that specifically.
Next, we hash the password passed to the function and compare it to the stored password. If it matches, we'll create the user object properties and start the session.
We start the session with the create_user_session function, which we've modified as follows:
Before, all we did was set user_id and username session variables, now we're doing a bit more. First off, when a user logs in, we regenerate any session ids. Next, we use the httputils control set a CSRF token. This will get passed along with any requests the user makes to prevent CSRF attacks. We're not storing the username anymore because we're going to be grabbing it from the database from now on. And finally, we set a session expire_time.
Our check_user_session has also become a little more complicated. Here it is.
Basically, what we're doing here is adding in several more checks for the user session. We start off by checking the CSRF token. Once we grab the CSRF token from the request, we can use the verifyCSRFToken in our HTTP utilities control to check it against the stored token. This is actually the function we're going to modify in DefaultHTTPUtilities to create our BlogHTTPUtilities class - the DefaultHTTPUtilities verifyCSRFToken method depends on some functionality that's not 100% implemented yet.
Next, we're just checking the expire time and clearing the session if it's been too long. Then we check the user id. If all this checks out, we update the session expiration time and return true - meaning the user is logged in with a valid session.
Next we have a couple of simple functions to handle our expire times. These are pretty self explanatory:
Now, we'll go over our retrieve_user function we called in the constructor. Again, really simple; we've seen functions almost exactly like this in our other classes.
Here is a simple write function I'm using to create a new user. There is no UI for it, but this will make it easier to create a user with the hashed password if you need it.
And finally, the salt and hash functions that will hash our passwords and generate a salt:
Let's talk briefly about the HTTPUtilities control. As I mentioned before, we need to make a fairly simple change to it in order to use the verifyCSRFToken function. We'll do that by copying the DefaultHTTPUtilities.php file to BlogHTTPUtilities and make our changes.
And that's all we need.
There are a few other simple changes we'll need in our controllers, just to handle our tokens and sessions. I've included these changes in the file archive above, but I'm just going to summarize them here, because they're very simple. Any time a user is logged in, we need to append the CSRF token to any links. We also begin using the logged_in user property instead of checking session variables directly. Finally, we stop depending on hidden form fields.
The last thing to think about is our post.php page. In the original version, we assumed that if a person made it to this page, that they must be logged in. Of course, that means that anyone who goes to post.php can add any content they want. So obviously, we need to check the user log in on post.php before we allow someone to post.
Where we're at, and where to go from here
So there you have it, a somewhat long-winded tutorial on using OWASP ESAPI PHP. We went over most of the features that are currently implemented in ESAPI PHP. On the whole, I would say that ESAPI PHP is well on its way to becoming a power house, but it's not quite ready to be implemented in most production environments as is. With a little work and customization, it definitely could be.
There are some great people working on the project, and I would encourage anyone with an interest in PHP security to get involved. You can find information on getting involved here.
As always, feedback, questions, comments are always appreciated. You can reach me at jackwillksecurity at gmail dot com or on Twitter (@jackwillk) where I will mostly be tweeting about total nonsense.
Part 1: We talked a little bit about the ESAPI PHP project, set up a plan for this series and wrote an extremely insecure basic blogging PHP web application.
Part 2: We set up the PHP ESAPI project and started validating user input.
Part 3: We started properly handling our output and used prepared statements to make our database queries safer.
And this week (our final entry in the series), we're going to go over securing our user authentication and sessions. There are some other great controls in ESAPI PHP, but the project is not as mature as the Java version and some of the controls are not quite ready for prime time. So I'm mostly going to stick with what can be implemented now with minimal changes. You can find the files from this week here.
Handling users and sessions
Let's start out listing what we'll be doing to harden our User class. This is not a comprehensive list on hardening a User control, but it includes items that will mostly touch on ESAPI controls in some way.
- Improve our session handling
- Implement CSRF tokens
- Hash user passwords
- Retrieve object properties from the database
- Expire user sessions after a set time
- Think about what we're storing in our session variables
That sounds like a lot, and it is. From the last post to this one, the User class has more than doubled in size. Luckily, none of it is too difficult. ESAPI PHP does have some objects and utilities for handling users. For the most part though, they're not complete. One thing it does have is very good utilities for handling CSRF tokens via the HTTPUtilties control. We're going to use that while we work on our User class.
We'll start out like we start out every other Class, by including the ESAPI controls we'll require. We require BlogHTTPUtilities because we're going to modify the DefaultHTTPUtilities just a little bit.
require_once("owasp/src/ESAPI.php");
require_once("owasp/src/reference/BlogHTTPUtilities.php");
We're also going to add a few new properties to our object:
private $esapi = null;
private $httputils = null;
private $logged_in = null;
private $salt = null;
private $expire_time = 600;
$esapi is our ESAPI object - we've seen this plenty of places before. We store the HTTPUtilities control in $httputils. Since we only have one level of user, we'll just use a boolean called $logged_in to store that information. $salt is used to improve our password hashing. And finally, $expire_time is the number of seconds of inactivity before a user's session expires.
One thing, we'll be storing the salt in the database, so let's alter our user table right now. Also, we'll be hashing the password so we need to make the password field a bit bigger.
ALTER TABLE user ADD salt VARCHAR(30);
alter table user change password password varchar(100) not null;
We'll just go function by function and go over what we're changing in the User class. First up is our constructor:
function __construct($user_id='') {
$this->esapi = new ESAPI("/var/www/insecure_week1/lib/owasp/test/testresources/ESAPI.xml");
ESAPI::setHTTPUtilities(new BlogHTTPUtilities());
$this->httputils = ESAPI::getHTTPUtilities();
$this->logged_in = false;
if($this->check_user_session()) {
if($this->retrieve_user()) {
$this->logged_in = true;
}
}
}
Not much we haven't seen before. We create our ESAPI controls and default them to not logged in. Then, we check to see if a user session exists and contains valid information. If so, we change logged_in to true. The retrieve_user function is new, and we'll go over that soon.
Next, we create a few simple get/set methods for our new properties:
function set_password($password) {
$this->password = $password;
}
function get_password() {
return $this->password;
}
function get_token() {
return $this->httputils->getCSRFToken();
}
function get_logged_in() {
return $this->logged_in;
}
function set_logged_in($logged_in) {
$this->logged_in = $logged_in;
}
The only thing that may not be obvious here is the get_token function. The HTTPUtilities controls handles most everything we need for CSRF tokens, so our getter function just calls HTTPUtilities'.
Now we get to the interesting stuff, starting with our login function:
function login($username, $password) {
$db = DB::get_instance();
$sql = $db->prepare("SELECT id, username, password, salt FROM user WHERE username = ?");
$sql->bind_param('s', $username);
if(!$sql->execute()) {
$this->error_list[] = "Could not log in.";
$sql->close();
return false;
}
$sql->store_result();
if($sql->num_rows() != 1) {
$this->error_list[] = "Could not log in.";
$sql->free_result();
return false;
}
$sql->bind_result($user_id, $username, $stored_pass, $salt);
$sql->fetch();
$sql->free_result();
$sql->close();
if($this->hash_pass($password, $salt) != $stored_pass) {
echo("<br>password = $password<br>salt = $salt<br>stored_pass = $stored_pass<br>hashed_pass = " . $this->hash_pass($password,$salt) . "<br>");
$this->error_list[] = "Invalid password.";
return false;
}
$this->user_id = $user_id;
$this->username = $username;
$this->password = $stored_pass;
$this->salt = $salt;
if(!$this->create_user_session()) {
return false;
}
return true;
}
The first thing you might notice is that if we're given a valid username, we're selecting all of the user's information from the database. This is because of the addition of the salt field. We need to hash the password the user submitted to us to compare the two. We might as well grab all the information in one fell swoop rather than making two database calls.
The next thing we'll noticed we changed a bit is the num_rows check. We know that this query should never return more than one row, so we should check for that specifically.
Next, we hash the password passed to the function and compare it to the stored password. If it matches, we'll create the user object properties and start the session.
We start the session with the create_user_session function, which we've modified as follows:
private function create_user_session() {
session_start();
if(!session_regenerate_id(true)) {
$this->error_list[] = "Could not create user session. Please try again";
return false;
}
$this->httputils->setCSRFToken();
$_SESSION['user_id'] = $this->user_id;
$_SESSION['expire_time'] = time() + $this->expire_time;
return true;
}
Before, all we did was set user_id and username session variables, now we're doing a bit more. First off, when a user logs in, we regenerate any session ids. Next, we use the httputils control set a CSRF token. This will get passed along with any requests the user makes to prevent CSRF attacks. We're not storing the username anymore because we're going to be grabbing it from the database from now on. And finally, we set a session expire_time.
Our check_user_session has also become a little more complicated. Here it is.
function check_user_session() {
session_start();
$token = $_GET['token'];
if(!$token) {
$token = $_POST['token'];
}
if(!$token) {
return false;
}
if(!$this->httputils->verifyCSRFToken($token)) {
$this->error_list[] = "Could not verify session.";
return false;
}
if(!$_SESSION['expire_time'] || time() > $_SESSION['expire_time']) {
$this->expire_session();
$this->error_list[] = "Session_expired";
return false;
}
if(!$_SESSION['user_id']) {
$this->error_list[] = "Session not found.";
return false;
} else {
$this->user_id = $_SESSION['user_id'];
}
$this->update_expire_time();
return true;
}
Basically, what we're doing here is adding in several more checks for the user session. We start off by checking the CSRF token. Once we grab the CSRF token from the request, we can use the verifyCSRFToken in our HTTP utilities control to check it against the stored token. This is actually the function we're going to modify in DefaultHTTPUtilities to create our BlogHTTPUtilities class - the DefaultHTTPUtilities verifyCSRFToken method depends on some functionality that's not 100% implemented yet.
Next, we're just checking the expire time and clearing the session if it's been too long. Then we check the user id. If all this checks out, we update the session expiration time and return true - meaning the user is logged in with a valid session.
Next we have a couple of simple functions to handle our expire times. These are pretty self explanatory:
function expire_session() {
session_destroy();
}
function update_expire_time() {
$_SESSION['expire_time'] = time() + $this->expire_time;
}
Now, we'll go over our retrieve_user function we called in the constructor. Again, really simple; we've seen functions almost exactly like this in our other classes.
private function retrieve_user() {
if(!$this->user_id) {
$this->error_list[] = "No user to retrieve!";
return false;
}
$db = DB::get_instance();
$sql = $db->prepare("SELECT username, password FROM user WHERE id = ?");
$sql->bind_param('i', $this->user_id);
if(!$sql->execute()) {
$this->error_list[] = "Could not retrieve user.";
$sql->close();
return false;
}
$sql->store_result();
if($sql->num_rows() != 1) {
$this->error_list[] = "Could not retrieve user";
$sql->free_result();
$sql->close();
return false;
}
$sql->bind_result($this->username, $this->password);
$sql->fetch();
$sql->free_result();
$sql->close();
return true;
}
Here is a simple write function I'm using to create a new user. There is no UI for it, but this will make it easier to create a user with the hashed password if you need it.
function write() {
$db = DB::get_instance();
if(!$this->salt) {
$this->gen_salt();
}
$hashed_pass = $this->hash_pass($this->password, $this->salt);
$sql = $db->prepare("INSERT INTO user (username, password, salt) VALUES (?, ?, ?)");
$sql->bind_param('sss', $this->username, $hashed_pass, $this->salt);
if(!$sql->execute()) {
$this->error_list[] = "Could not write user, please try again.";
$sql->close();
return false;
}
$sql->close();
return true;
}
And finally, the salt and hash functions that will hash our passwords and generate a salt:
private function gen_salt() {
$this->salt = rand(1,100000) . time() . $this->username . rand(1,100000);
}
private function hash_pass($password, $salt) {
return md5($password.$salt);
}
Let's talk briefly about the HTTPUtilities control. As I mentioned before, we need to make a fairly simple change to it in order to use the verifyCSRFToken function. We'll do that by copying the DefaultHTTPUtilities.php file to BlogHTTPUtilities and make our changes.
public function verifyCSRFToken($token) {
if(!$this->getCSRFToken() == $token) {
throw new IntrusionException('Authentication failed.', 'Possibly forged HTTP request without proper CSRF token detected.');
return false;
}
return true;
}
And that's all we need.
There are a few other simple changes we'll need in our controllers, just to handle our tokens and sessions. I've included these changes in the file archive above, but I'm just going to summarize them here, because they're very simple. Any time a user is logged in, we need to append the CSRF token to any links. We also begin using the logged_in user property instead of checking session variables directly. Finally, we stop depending on hidden form fields.
The last thing to think about is our post.php page. In the original version, we assumed that if a person made it to this page, that they must be logged in. Of course, that means that anyone who goes to post.php can add any content they want. So obviously, we need to check the user log in on post.php before we allow someone to post.
Where we're at, and where to go from here
So there you have it, a somewhat long-winded tutorial on using OWASP ESAPI PHP. We went over most of the features that are currently implemented in ESAPI PHP. On the whole, I would say that ESAPI PHP is well on its way to becoming a power house, but it's not quite ready to be implemented in most production environments as is. With a little work and customization, it definitely could be.
There are some great people working on the project, and I would encourage anyone with an interest in PHP security to get involved. You can find information on getting involved here.
As always, feedback, questions, comments are always appreciated. You can reach me at jackwillksecurity at gmail dot com or on Twitter (@jackwillk) where I will mostly be tweeting about total nonsense.
Monday, July 12, 2010
Using the OWASP PHP ESAPI - Part 3
And here we are at the 3rd part of the OWASP ESAPI PHP tutorial series. If you haven't read the first two yet, you can find Part One here and Part Two here. This week we're going to cover encoding our output, and making our database queries safer. Let's get right into it. This week's files can be found here.
Encoding our output
We've seen the encoder in use a little bit so far - we've used it canonicalize our input. This week, we're going to use it to encode user submitted output so that we can display it safely. Our blog app is extremely vulnerable to XSS attacks right now; for example, any commentor can post malicious code. By encoding our output before we display it we can significantly cut that risk.
First step, let's examine our application and figure out where we're outputting any user input. Of our four pieces, only one of them displays user submitted data; index.php.
Last week, we said that we would not allow the user to add actual functioning HTML or JavaScript into their comments or posts, so if there is any of that, we're just going to encode it to display safely on the screen. So basically, anywhere in index.php that we're displaying something a user submitted, we're going to run it through the DefaultEncoder's encodeForHTML function. All of that is contained in our construct_content_display function, which will now look like this:
And that's all there is to it! There are lots of other useful functions in the Encoder security control and I highly encourage you to check them all out.
Making our database queries safe
The other thing we're going to go over this week isn't completely ESAPI PHP centric. There are several tactics we could use to protect ourselves against SQL injection attacks and generally make them more error-resistant. We could use the ESAPI PHP encoder to escape our queries, but the ESAPI PHP docs for the encodeForSQL function state it best when they say:
"This method is not recommended. The use of the PreparedStatement interface is the preferred approach. However, if for some reason this is impossible, then this method is provided as a weaker alternative."
We're going to depart slightly from the ESAPI PHP for a moment to discuss using prepared statements. Prepared statements are used with the mysqli built in PHP library. They allow you to completely separate the data from the SQL making queries much safer by default. Using prepared statements is not any more difficult than using the standard PHP mysql functions, it just requires you to think a little differently. If you haven't used mysqli, you can find all the documentation here.
First thing we're going to do is modify our DB class to use mysqli now. Our new class looks like this:
Simple! You'll notice the other thing we did was create a new user for our app which does not have root privileges. When you create the user, it's important to give the user the minimum possible privilege.
The basic steps involved in using a prepared statement are as follows:
Let's look at a simple example, our get_all_content function in index.php. Here's the original:
And our new function looks like:
We prepare the statement with the prepare function and execute. After the query executes, we use the bind_result function to bind the id column to a variable called $id. The while loop using the fetch function will assign the values for each column into the variables we defined in the bind_result function, so from here we just use the setter methods to set up our object. After we've looped through our result, we clean up by closing our sql statement.
And we're going to do the same thing to our get_all_comments function:
Now we move onto our three classes. We'll start with Content.php. We use SQL queries in two of our functions, retrieve_content and write. Our new retrieve_content function looks like this:
One nice thing about the bind_result function is if we're only querying for one row, we can assign the result directly to our object properties. We're also adding some error checking into our function.
Now our write function. This is the first query where we'll actually be writing to the database with our prepared statement.
Nothing too different then we've seen before. We're going to move onto the Comment class, the functions are nearly identical, retrieve_comment and write. retrieve_comment looks like:
And write looks like:
And finally, our User class. We're going to add more error handling to our class, so we're going to add the same error_list property we added to the Comment and Content classes last week. So, in the property list at the top of the class:
And our two handler functions:
Now, we improve our login function query and add better error handling:
Simple! Now, the last thing we'll do is edit our controllers to handle the new errors we're generating in our classes. So, in post.php, we'll check the return value of write() and display the error if it exists. Here's the relevant portion of post.php
The added error handling in comment.php looks like this:
And finally, our new login page looks like this.
And we add the error list display code to the login html page:
And that's it for this week! This is a busy time for me - meaning it will probably be a little longer until the next post. Next time, we'll be covering logging, form security and general hardening. There's going to be one or two more posts in this series - we'll see how it goes. I would love to hear some feedback on this blog, so feel free to leave a comment, send me an email (jackwillksecurity at gmail dot com) or reach out to me on Twitter (@jackwillk)
Encoding our output
We've seen the encoder in use a little bit so far - we've used it canonicalize our input. This week, we're going to use it to encode user submitted output so that we can display it safely. Our blog app is extremely vulnerable to XSS attacks right now; for example, any commentor can post malicious code. By encoding our output before we display it we can significantly cut that risk.
First step, let's examine our application and figure out where we're outputting any user input. Of our four pieces, only one of them displays user submitted data; index.php.
Last week, we said that we would not allow the user to add actual functioning HTML or JavaScript into their comments or posts, so if there is any of that, we're just going to encode it to display safely on the screen. So basically, anywhere in index.php that we're displaying something a user submitted, we're going to run it through the DefaultEncoder's encodeForHTML function. All of that is contained in our construct_content_display function, which will now look like this:
function construct_content_display($content_arr) {
$output = '';
$encoder = ESAPI::getEncoder();
for($i=0;$i<count($content_arr);$i++) {
$output .= "<p><b>" . $encoder->encodeForHTML($content_arr[$i]->get_title()) . "</b></p>";
$output .= "<br><p>" . $encoder->encodeForHTML($content_arr[$i]->get_content()) . "</p>";
$comment_arr = get_all_comments($content_arr[$i]->get_content_id());
for($j=0;$j<count($comment_arr);$j++) {
$output .= "<br><p>" . $encoder->encodeForHTML($comment_arr[$j]->get_comment()) . " - " . $encoder->encodeForHTML($comment_arr[$j]->get_date_created()) . "</p>";
}
$output .= "<br><a href=\"comment.php?content_id=" . $content_arr[$i]->get_content_id() . "\">Comment</a>";
}
return $output;
}
And that's all there is to it! There are lots of other useful functions in the Encoder security control and I highly encourage you to check them all out.
Making our database queries safe
The other thing we're going to go over this week isn't completely ESAPI PHP centric. There are several tactics we could use to protect ourselves against SQL injection attacks and generally make them more error-resistant. We could use the ESAPI PHP encoder to escape our queries, but the ESAPI PHP docs for the encodeForSQL function state it best when they say:
"This method is not recommended. The use of the PreparedStatement interface is the preferred approach. However, if for some reason this is impossible, then this method is provided as a weaker alternative."
We're going to depart slightly from the ESAPI PHP for a moment to discuss using prepared statements. Prepared statements are used with the mysqli built in PHP library. They allow you to completely separate the data from the SQL making queries much safer by default. Using prepared statements is not any more difficult than using the standard PHP mysql functions, it just requires you to think a little differently. If you haven't used mysqli, you can find all the documentation here.
First thing we're going to do is modify our DB class to use mysqli now. Our new class looks like this:
<?php
/**
*
* DB.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment.
*
* @author jackwillk
* @created 2010
*
*/
class DB {
/**
* This class deals with mysqli singleton
*/
private $host = "localhost";
private $username = "insecureapp";
private $password = "supersecretpw";
private $db_name = "insecure";
private static $instance;
private function __construct() {
$this->connection = new mysqli($this->host, $this->username, $this->password, $this->db_name);
}
static function get_instance() {
if(!self::$instance) {
self::$instance = new DB;
}
return self::$instance->connection;
}
}
?>
Simple! You'll notice the other thing we did was create a new user for our app which does not have root privileges. When you create the user, it's important to give the user the minimum possible privilege.
The basic steps involved in using a prepared statement are as follows:
- Prepare the statement
- Bind parameters
- Execute the query
Let's look at a simple example, our get_all_content function in index.php. Here's the original:
function get_all_content() {
$db = DB::get_instance();
$sql = "SELECT id FROM content order by date_created";
$result = $db->query($sql);
while($row=$db->fetch_assoc($result)) {
$content_arr[] = new Content($row['id']);
}
return $content_arr;
}
And our new function looks like:
function get_all_content() {
$db = DB::get_instance();
$sql = $db->prepare("SELECT id, user_id, title, content, date_created FROM content ORDER BY date_created");
$sql->execute();
$sql->bind_result($id, $user_id, $title, $content, $date_created);
while($sql->fetch()) {
$post = new Content();
$post->set_content_id($id);
$post->set_user_id($user_id);
$post->set_title($title);
$post->set_content($content);
$post->set_date_created($date_created);
$content_arr[] = $post;
}
$sql->close();
return $content_arr;
}
We prepare the statement with the prepare function and execute. After the query executes, we use the bind_result function to bind the id column to a variable called $id. The while loop using the fetch function will assign the values for each column into the variables we defined in the bind_result function, so from here we just use the setter methods to set up our object. After we've looped through our result, we clean up by closing our sql statement.
And we're going to do the same thing to our get_all_comments function:
function get_all_comments($content_id) {
$db = DB::get_instance();
$sql = $db->prepare("SELECT id, comment, content_id, date_created FROM comments WHERE content_id = ? order by date_created");
$sql->bind_param('i', $content_id);
$sql->execute();
$sql->bind_result($id, $comment_body, $content_id, $date_created);
while($sql->fetch()) {
$comment = new Comment();
$comment->set_comment_id($id);
$comment->set_comment($comment_body);
$comment->set_content_id($content_id);
$comment->set_date_created($date_created);
$comment_arr[] = $comment;
}
$sql->close();
return $comment_arr;
}
Now we move onto our three classes. We'll start with Content.php. We use SQL queries in two of our functions, retrieve_content and write. Our new retrieve_content function looks like this:
private function retrieve_content($content_id) {
$db = DB::get_instance();
$sql = $db->prepare("SELECT id, user_id, title, content, date_created FROM content WHERE id = ?");
$sql->bind_param('i', $content_id);
if(!$sql->execute()) {
$this->error_list[] = "Could not retrieve content.";
$sql->close;
return false;
}
if(!$sql->num_rows() == 0) {
$this->error_list[] = "Content not found.";
$sql->close;
return false;
}
$sql->bind_result($this->content_id, $this->user_id, $this->title, $this->content, $this->date_created);
$sql->fetch();
$sql->close();
return true;
}
One nice thing about the bind_result function is if we're only querying for one row, we can assign the result directly to our object properties. We're also adding some error checking into our function.
Now our write function. This is the first query where we'll actually be writing to the database with our prepared statement.
function write() {
$db = DB::get_instance();
$sql = $db->prepare("INSERT INTO content (user_id, title, content, date_created) values (?, ?, ?, ?)");
$sql->bind_param('isss', $this->user_id, $this->title, $this->content, date("Y-m-d"));
if(!$sql->execute()) {
$this->error_list[] = "Could not save post, please try again.";
$sql->close();
return false;
}
$sql->close();
return true;
}
Nothing too different then we've seen before. We're going to move onto the Comment class, the functions are nearly identical, retrieve_comment and write. retrieve_comment looks like:
function retrieve_comment($comment_id) {
$db = DB::get_instance();
$sql = $db->prepare("SELECT id, comment, content_id, date_created from comments where id = ?");
$sql->bind_param('i', $comment_id);
if(!$sql->execute()) {
$this->error_list[] = "Could not retrieve comment";
$sql->close();
return false;
}
$sql->bind_result($this->comment_id, $this->comment, $this->content_id, $this->date_created);
$sql->fetch();
$sql->close();
}
And write looks like:
function write() {
$db = DB::get_instance();
$sql = $db->prepare("INSERT INTO comments (comment, content_id, date_created) VALUES (?, ?, ?)");
$sql->band_param("sis", $this->comment, $this->content_id, date("Y-m-d"));
if(!$sql->execute) {
$this->error_list[] = "Could not write comment.";
$sql->close();
return false;
}
$sql->close();
return true;
}
And finally, our User class. We're going to add more error handling to our class, so we're going to add the same error_list property we added to the Comment and Content classes last week. So, in the property list at the top of the class:
private $error_list = null;
And our two handler functions:
function clear_error_list() {
$this->error_list = null;
}
function get_error_list() {
return $this->error_list;
}
Now, we improve our login function query and add better error handling:
function login($username, $password) {
$db = DB::get_instance();
$sql = $db->prepare("SELECT id, username, password FROM user WHERE username = ? AND password = ?");
$sql->bind_param('ss', $username, $password);
if(!$sql->execute()) {
$this->error_list[] = "Could not login.";
$sql->close();
return false;
}
if($sql->num_rows() == 0) {
$this->error_list[] = "Username or password not found";;
$sql->close();
return false;
}
$sql->bind_result($this->user_id, $this->username, $this->password);
$sql->fetch();
$this->create_user_session();
return true;
}
Simple! Now, the last thing we'll do is edit our controllers to handle the new errors we're generating in our classes. So, in post.php, we'll check the return value of write() and display the error if it exists. Here's the relevant portion of post.php
<?php
if($_POST['submit']) {
$content = new Content();
$content->set_user_id($_POST['user_id']);
$content->set_title($_POST['title']);
$content->set_content($_POST['content']);
$error_list = $content->get_error_list();
if(!count($error_list)) {
if($content->write()) {
header("Location:index.php");
} else {
$error_list = $content->get_error_list();
}
}
}
?>
The added error handling in comment.php looks like this:
if($_POST['submit']) {
//save the comment
$comment = new Comment();
$comment->set_comment($_POST['comment']);
$comment->set_content_id($_POST['content_id']);
$error_list = $comment->get_error_list();
if(!count($error_list)) {
if($comment->write()) {
header("Location:index.php");
} else {
$error_list = $comment->get_error_list();
}
}
}
And finally, our new login page looks like this.
<?php
/**
*
* login.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require("lib/DB.php");
require("lib/User.php");
$user = new User();
if($user->get_user_id()) {
echo("You are already logged in.");
exit();
}
if($_POST['submit']) {
if($user->login($_POST['username'], $_POST['password'])) {
header("Location:index.php");
} else {
$error_list = $user->get_error_list();
}
}
include("login.html");
if($error_list) {
$content->clear_error_list();
}
?>
And we add the error list display code to the login html page:
<html>
<head>
<title>Login</title>
</head>
<body>
<?php if($error_list) {
for($i=0;$i<count($error_list);$i++) {
?>
<font color="red"><?= $error_list[$i] ?></font><br>
<?php } } ?>
<b>This is my awesome login page!</b>
<form name="login" method="post" action="login.php">
username:<input type="text" name="username"><br>
password:<input type="text" name="password"><br>
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
And that's it for this week! This is a busy time for me - meaning it will probably be a little longer until the next post. Next time, we'll be covering logging, form security and general hardening. There's going to be one or two more posts in this series - we'll see how it goes. I would love to hear some feedback on this blog, so feel free to leave a comment, send me an email (jackwillksecurity at gmail dot com) or reach out to me on Twitter (@jackwillk)
Saturday, July 3, 2010
Using the OWASP PHP ESAPI - Part 2
After thinking about this for a week, I've decided to break this series out over a few more weeks. Rather than rush through things, I'm going to take my time and give a better overview. So, this week, I'm going to go over setting up ESAPI PHP and implementing the Validator security control. If you haven't read the first post in this series, you can find it here.
Setting up ESAPI
Setting up ESAPI PHP is very simple. Just check out the code from their Google Code repository like so:
Go ahead and change permissions/ownership for your own particular setup. Then just move ESAPI.xml out of your document root. We'll be modifying ESAPI.xml a little bit later, but the defaults are fine to start with. I'm actually not going to move my ESAPI.xml file (so you'll see it referenced in the ESAPI instantiation calls) just so that when we modify it, I can include it in the download link. You wouldn't want to put this into production without moving it however.
A few setup notes
We're going to use the reference objects wherever possible in ESAPI PHP (located in src/reference). They are simple implementations of the interfaces defined in src/ and for our application, we can mostly use them with minimal modifications.
We're going to use the DefaultSecurityConfiguration class, but we need to address a small bug before we can use it. So open src/reference/DefaultSecurityConfiguration.php and do a search replace. You will be replacing $this->logSpecial with $this->_logSpecial. It's incorrectly referred to in seven places.
We're also going to make a small change to the _logSpecial function - because it just prints an error to the screen, which we don't want to happen. Let's log it instead. So change the _logSpecial function in DefaultSecurityConfiguration.php to:
I did email the ESAPI PHP mailing list about the function name issue so hopefully it will be making it into trunk in the near future.
Customizing the DefaultValidator class for our application
We're going to custom the DefaultValidator in src/reference for our application. The validator security control is used to check pieces of data against criteria we define. We can see all the documentation in the phpdocs or in the Validator.php interface in src/.
There's one thing I want to change in the DefaultValidator class. When a check fails, it throws a ValidationException that contains a message that's safe to display to the end user. The DefaultValidator catches that exception but only returns false when it catches it. Because of this, when I come back to notify the user that there was a problem with their input, I can't give them a specific reason for the failure. For example, if I check an integer input to verify that it's between 1 and 20 and the check fails, I won't know if the user entered 0, 50, or "cat". I'd like to capture that exception so that we can display the safe error message to the end user.
So we'll start by saving a copy of DefaultValidator.php as BlogValidator.php. We're going to define a new private property, $_lastError. Just put the following lines where we define the rest of the object properties.
And then we need to add two methods to handle this property. getLastError to return the last error message and clearLastError to set the lastError property back to null. We're never going to set this property from anywhere but inside the BlogValidator class, but we will want to clear it after we display its value.
Simple, huh? Next, we need to set this property wherever we catch an error. So in any public functions where we see
We're going to change it to:
And that's it! Now let's see how we would go about implementing our BlogValidator in our Content class. First, we need to include ESAPI.php and our BlogValidator.php files, so at the top of our file, we'll put:
Next we need to add some new class properties for the ESAPI controls and an error list property to hold our ValidationException error messages. Our new property declarations now look like this:
Now we'll get our ESAPI objects set up in the constructor. It's really easy. We're just using the ESAPI security control getter methods to set up the two security controls we'll be using this week. We're also going to modify our constructor just a little bit from the first week to make it easier to control the flow of our validation. We're no longer going to allow the calling method to pass all the properties into the constructor, they should use the getter/setter methods.
All we're doing is creating an ESAPI object and grabbing the Encoder and Validator objects. We haven't really talked about the Encoder object yet, but for now we're just going to be using it canonicalize user input before validating.
Now, we have this $error_list property, we need to handle it in basically the same we handled it in the BlogValidator class. One function to retrieve the error list, and one function to clear it.
Next, we'll tackle our setter methods. One thing you may have noticed is that we don't use all of these methods in our application. We could get rid of a couple of them with no effect on our app. That doesn't mean we necessarily want to get rid of them, or to ignore them when we're securing our application. At some point, we may want to use these methods as we're adding new features to our app, or if we're passing this off to another developer, who knows what they'll want to do. If we think about all of our input now, it makes our intentions clearer, and helps us to future proof the application. Also, it's less work later because the ESAPI is fresh in our minds, it's less to think about later.
Let's just go in order; we have the following functions.
There's an important step we missed the first time around, and unfortunately it's all too common for developers to miss this step. We never really thought about the kinds of values our properties should hold. We very much just slapped a bunch of crap together without much forethought into the rules that define our application. For example, we set a 140 character limit on post titles, is this reasonable? Who knows! Do we want to allow comments to put javascript in their comments? Probably not, but we didn't think about it. The only place we've really defined our object properties was in the database when we created our tables. If you think about these kind of things and define them when you're designing your application, you're a good bit of the way there.
First up, content_id. If we look at how we defined it in our database, it's a signed int, it's a primary key (so it needs to be unique), it can't be null, and MySQL will auto increment it for us if it's not defined. Now that we're considering what kind of values our object properties should contain, maybe we should start by changing our database's definition of the property.
A signed int in MySQL has a range of -2,147,483,648 to 2,147,438,648. I don't see any possible situation where we would want a negative number for an id, so maybe we should use an unsigned int. And as much as everyone's going to love reading your blog, 2 billion posts might be a bit much. Maybe we don't actually need an int where a smallint would do.
The content id is a unique id that identifies that particular piece of content, so setting it as a primary key sounds like a good idea. Finally, auto_increment is fine for a field like this because we're not particularly worried about anyone figuring out how are posts are labeled and we won't have to worry about generating unique ids.
So let's alter our database definition:
Now let's just enforce this in our setter method.
The only thing in our new and improved set_content_id method that you haven't seen before is the canonicalize function. This function performs canonicalization on data to ensure that it has been to its most basic form before validation (taken straight from the phpdoc). Basically, if a user is passing encoded content, this will reduce it to a form that can be validated. The encoder will throw an IntrusionException if certain problems are detected, so we need to catch that and handle it. This is why we're wrapping the canonicalize function in our Content class like so:
We're going to do the same thing with the rest of the content properties. Our user_id is going to be the same deal as the content_id (minus the primary key and auto increment pieces). Because this user_id in content references the id in the user table, we're going to edit it there too for consistency.
Our new set_user_id function looks like this:
Our title property gets a little interesting. When we created our content table in the database, we defined that title as a 140 character string. We said it was OK if it was null. 140 characters seems like a reasonable limit for a post title, so we're going to keep that. But we probably don't really want it to ever be null, so we'll alter the table again.
When we're dealing with a number, it's a fairly simple matter to make sure that a user's not slipping something bad in there. With text, it's a little different. We also have to consider the output. For our simple blog, we're going to establish a few guidelines concerning the title. The post cannot insert any working HTML or javascript into their post title. A post can refer to HTML or javascript in their post titles.
This means that we want to make sure that the user input exists, doesn't exceed the maximum length and doesn't contain any non-printable characters. Beyond that, we're going to allow them to put whatever they would like and let the encoder and sanitizer objects take care of making it safe wherever we're using this property. We'll allow a user to input something potentially malicious, but we'll handle it in a safe way at our trust boundaries.
Our title setter function is going to look like this:
We're essentially going to do the same exact thing for our content property. We're going to alter the database so it can't be null:
and define our setter function to ensure that it only contains printable characters.
Finally, we have our date_created setter. We're just going to verify that we're using a valid date. No need to change the database here.
We're using the validator's isValidDate function, which checks that the date passed matches the format given. The format is the same as the format used in the built in PHP date function.
So there we go, our Content class now validates its input. Here's our current Content class in full.
Let's put our Validator to good use and edit the controllers where we call these functions to deal with the errors. Lucky for us, that all takes place in one file, post.php. Because we've put so much of this into our classes, it only takes the addition of a few lines of code in our controller to implement our changes. Our new post.php looks like this:
And we'll edit post.html to display a list of the errors. Again, we're just adding a few easy lines of code.
We'll do the same thing with our Comment class/controller/html. In our Comment class, we're going to do parameter validation, create an error list, and improve our constructor a little bit. This should all pretty much make sense if you read the Content class portion. The new Comment class looks like this:
Our new comment.php controller looks like this:
And it's html with the error list looks like:
I think we've done quite a bit for this week. Next week, we're going to go into detail about how to handle our output and create safe database queries. You can find a link to the current project here - I didn't include the entire ESAPI PHP project, just our customized files, DefaultSecurityConfiguration.php and BlogValidator.php. They're in the main directory, you can move them into place after you check out the ESAPI PHP code.
As always, I'd love to hear feedback on this post. You can reach me at @jackwillk on Twitter, jackwillksecurity at gmail dot com, or leave a comment at the link below.
Setting up ESAPI
Setting up ESAPI PHP is very simple. Just check out the code from their Google Code repository like so:
jk@jk-laptop:/var/www/insecure_week2/lib$ svn checkout http://owasp-esapi-php.googlecode.com/svn/trunk/ owasp
A owasp/test
A owasp/test/http
A owasp/test/http/TestHttpServletRequest.php
A owasp/test/http/TestRequestDispatcher.php
...
Checked out revision 812.
Go ahead and change permissions/ownership for your own particular setup. Then just move ESAPI.xml out of your document root. We'll be modifying ESAPI.xml a little bit later, but the defaults are fine to start with. I'm actually not going to move my ESAPI.xml file (so you'll see it referenced in the ESAPI instantiation calls) just so that when we modify it, I can include it in the download link. You wouldn't want to put this into production without moving it however.
A few setup notes
We're going to use the reference objects wherever possible in ESAPI PHP (located in src/reference). They are simple implementations of the interfaces defined in src/ and for our application, we can mostly use them with minimal modifications.
We're going to use the DefaultSecurityConfiguration class, but we need to address a small bug before we can use it. So open src/reference/DefaultSecurityConfiguration.php and do a search replace. You will be replacing $this->logSpecial with $this->_logSpecial. It's incorrectly referred to in seven places.
We're also going to make a small change to the _logSpecial function - because it just prints an error to the screen, which we don't want to happen. Let's log it instead. So change the _logSpecial function in DefaultSecurityConfiguration.php to:
private function _logSpecial($msg)
{
ESAPI::getAuditor('DefaultSecurityConfiguration')->warning(Auditor::SECURITY, false, $msg);
}
I did email the ESAPI PHP mailing list about the function name issue so hopefully it will be making it into trunk in the near future.
Customizing the DefaultValidator class for our application
We're going to custom the DefaultValidator in src/reference for our application. The validator security control is used to check pieces of data against criteria we define. We can see all the documentation in the phpdocs or in the Validator.php interface in src/.
There's one thing I want to change in the DefaultValidator class. When a check fails, it throws a ValidationException that contains a message that's safe to display to the end user. The DefaultValidator catches that exception but only returns false when it catches it. Because of this, when I come back to notify the user that there was a problem with their input, I can't give them a specific reason for the failure. For example, if I check an integer input to verify that it's between 1 and 20 and the check fails, I won't know if the user entered 0, 50, or "cat". I'd like to capture that exception so that we can display the safe error message to the end user.
So we'll start by saving a copy of DefaultValidator.php as BlogValidator.php. We're going to define a new private property, $_lastError. Just put the following lines where we define the rest of the object properties.
private $_lastError = null;
And then we need to add two methods to handle this property. getLastError to return the last error message and clearLastError to set the lastError property back to null. We're never going to set this property from anywhere but inside the BlogValidator class, but we will want to clear it after we display its value.
/**
* Clears the last error
*
* @return does not return a value
*/
public function clearLastError() {
$this->_lastError = null;
}
/**
* Gets the lastError property
*
* @return string lastError property
*/
public function getLastError() {
return $this->_lastError;
}
Simple, huh? Next, we need to set this property wherever we catch an error. So in any public functions where we see
catch ( Exception $e)
{
return false;
}
We're going to change it to:
catch ( Exception $e )
{
$this->lastError = $e->getUserMessage();
return false;
}
And that's it! Now let's see how we would go about implementing our BlogValidator in our Content class. First, we need to include ESAPI.php and our BlogValidator.php files, so at the top of our file, we'll put:
require_once("owasp/src/ESAPI.php");
require_once("owasp/src/reference/BlogValidator.php");
Next we need to add some new class properties for the ESAPI controls and an error list property to hold our ValidationException error messages. Our new property declarations now look like this:
private $content_id = null;
private $user_id = null;
private $title = null;
private $content = null;
private $date_created = null;
private $esapi = null;
private $encoder = null;
private $validator = null;
private $error_list = null;
Now we'll get our ESAPI objects set up in the constructor. It's really easy. We're just using the ESAPI security control getter methods to set up the two security controls we'll be using this week. We're also going to modify our constructor just a little bit from the first week to make it easier to control the flow of our validation. We're no longer going to allow the calling method to pass all the properties into the constructor, they should use the getter/setter methods.
function __construct($content_id = '') {
$this->esapi = new ESAPI("/var/www/insecure_week2/lib/owasp/test/testresources/ESAPI.xml");
ESAPI::setEncoder(new DefaultEncoder());
ESAPI::setValidator(new BlogValidator());
$this->encoder = ESAPI::getEncoder();
$this->validator = ESAPI::getValidator();
if($content_id) {
$this->retrieve_content($content_id);
}
}
All we're doing is creating an ESAPI object and grabbing the Encoder and Validator objects. We haven't really talked about the Encoder object yet, but for now we're just going to be using it canonicalize user input before validating.
Now, we have this $error_list property, we need to handle it in basically the same we handled it in the BlogValidator class. One function to retrieve the error list, and one function to clear it.
function clear_error_list() {
$this->error_list = null;
}
function get_error_list() {
return $this->error_list;
}
Next, we'll tackle our setter methods. One thing you may have noticed is that we don't use all of these methods in our application. We could get rid of a couple of them with no effect on our app. That doesn't mean we necessarily want to get rid of them, or to ignore them when we're securing our application. At some point, we may want to use these methods as we're adding new features to our app, or if we're passing this off to another developer, who knows what they'll want to do. If we think about all of our input now, it makes our intentions clearer, and helps us to future proof the application. Also, it's less work later because the ESAPI is fresh in our minds, it's less to think about later.
Let's just go in order; we have the following functions.
- set_content_id
- set_user_id
- set_title
- set_content
- set_date_created
There's an important step we missed the first time around, and unfortunately it's all too common for developers to miss this step. We never really thought about the kinds of values our properties should hold. We very much just slapped a bunch of crap together without much forethought into the rules that define our application. For example, we set a 140 character limit on post titles, is this reasonable? Who knows! Do we want to allow comments to put javascript in their comments? Probably not, but we didn't think about it. The only place we've really defined our object properties was in the database when we created our tables. If you think about these kind of things and define them when you're designing your application, you're a good bit of the way there.
First up, content_id. If we look at how we defined it in our database, it's a signed int, it's a primary key (so it needs to be unique), it can't be null, and MySQL will auto increment it for us if it's not defined. Now that we're considering what kind of values our object properties should contain, maybe we should start by changing our database's definition of the property.
A signed int in MySQL has a range of -2,147,483,648 to 2,147,438,648. I don't see any possible situation where we would want a negative number for an id, so maybe we should use an unsigned int. And as much as everyone's going to love reading your blog, 2 billion posts might be a bit much. Maybe we don't actually need an int where a smallint would do.
The content id is a unique id that identifies that particular piece of content, so setting it as a primary key sounds like a good idea. Finally, auto_increment is fine for a field like this because we're not particularly worried about anyone figuring out how are posts are labeled and we won't have to worry about generating unique ids.
So let's alter our database definition:
alter table content change column id id smallint signed auto_increment;
Now let's just enforce this in our setter method.
function set_content_id($content_id) {
$content_id = $this->canonicalize($content_id);
if($this->validator->isValidNumber("Content ID", $content_id, 1, 65535, false)) {
$this->content_id = $content_id;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
The only thing in our new and improved set_content_id method that you haven't seen before is the canonicalize function. This function performs canonicalization on data to ensure that it has been to its most basic form before validation (taken straight from the phpdoc). Basically, if a user is passing encoded content, this will reduce it to a form that can be validated. The encoder will throw an IntrusionException if certain problems are detected, so we need to catch that and handle it. This is why we're wrapping the canonicalize function in our Content class like so:
function canonicalize($input) {
try {
$input = $this->encoder->canonicalize($input);
} catch (IntrusionException $e) {
echo($e->getUserMessage());
exit();
}
return $input;
}
We're going to do the same thing with the rest of the content properties. Our user_id is going to be the same deal as the content_id (minus the primary key and auto increment pieces). Because this user_id in content references the id in the user table, we're going to edit it there too for consistency.
alter table content change column user_id user_id smallint signed;
alter table user change column id id smallint signed auto_increment;
Our new set_user_id function looks like this:
function set_user_id($user_id) {
$user_id = $this->canonicalize($user_id);
if($this->validator->isValidNumber("User ID", $user_id, 1, 10000, false)) {
$this->user_id = $user_id;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
Our title property gets a little interesting. When we created our content table in the database, we defined that title as a 140 character string. We said it was OK if it was null. 140 characters seems like a reasonable limit for a post title, so we're going to keep that. But we probably don't really want it to ever be null, so we'll alter the table again.
alter table content change column title title varchar(140) not null;
When we're dealing with a number, it's a fairly simple matter to make sure that a user's not slipping something bad in there. With text, it's a little different. We also have to consider the output. For our simple blog, we're going to establish a few guidelines concerning the title. The post cannot insert any working HTML or javascript into their post title. A post can refer to HTML or javascript in their post titles.
This means that we want to make sure that the user input exists, doesn't exceed the maximum length and doesn't contain any non-printable characters. Beyond that, we're going to allow them to put whatever they would like and let the encoder and sanitizer objects take care of making it safe wherever we're using this property. We'll allow a user to input something potentially malicious, but we'll handle it in a safe way at our trust boundaries.
Our title setter function is going to look like this:
function set_title($title) {
$title = $this->canonicalize($title);
if($this->validator->isValidPrintable("Post Title", $title, 140, false)) {
$this->title = $title;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
We're essentially going to do the same exact thing for our content property. We're going to alter the database so it can't be null:
alter table content change column content content text not null;
and define our setter function to ensure that it only contains printable characters.
function set_content($content) {
$content = $this->canonicalize($content);
if($this->validator->isValidPrintable("Post Content", $content, 65535, false)) {
$this->content = $content;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
Finally, we have our date_created setter. We're just going to verify that we're using a valid date. No need to change the database here.
function set_date_created($date_created) {
$date_created = $this->canonicalize($date_created);
if($this->validator->isValidDate("Content date created", $date_created, "Y-m-d H:i:s", false)) {
$this->date_created = $date_created;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
We're using the validator's isValidDate function, which checks that the date passed matches the format given. The format is the same as the format used in the built in PHP date function.
So there we go, our Content class now validates its input. Here's our current Content class in full.
<?php
/**
*
* Content.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require_once("owasp/src/ESAPI.php");
require_once("owasp/src/reference/BlogValidator.php");
class Content {
private $content_id = null;
private $user_id = null;
private $title = null;
private $content = null;
private $date_created = null;
private $esapi = null;
private $encoder = null;
private $validator = null;
private $error_list = null;
function __construct($content_id = '') {
$this->esapi = new ESAPI("/var/www/insecure_week2/lib/owasp/test/testresources/ESAPI.xml");
ESAPI::setEncoder(new DefaultEncoder());
ESAPI::setValidator(new BlogValidator());
$this->encoder = ESAPI::getEncoder();
$this->validator = ESAPI::getValidator();
if($content_id) {
$this->retrieve_content($content_id);
}
}
function get_content_id() {
return $this->content_id;
}
function set_content_id($content_id) {
$content_id = $this->canonicalize($content_id);
if($this->validator->isValidNumber("Content ID", $content_id, 1, 65535, false)) {
$this->content_id = $content_id;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function get_user_id() {
return $this->user_id;
}
function set_user_id($user_id) {
$user_id = $this->canonicalize($user_id);
if($this->validator->isValidNumber("User ID", $user_id, 1, 10000, false)) {
$this->user_id = $user_id;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function get_title() {
return $this->title;
}
function set_title($title) {
$title = $this->canonicalize($title);
if($this->validator->isValidPrintable("Post Title", $title, 140, false)) {
$this->title = $title;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function get_content() {
return $this->content;
}
function set_content($content) {
$content = $this->canonicalize($content);
if($this->validator->isValidPrintable("Post Content", $content, 65535, false)) {
$this->content = $content;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function get_date_created() {
return $this->date_created();
}
function set_date_created($date_created) {
$date_created = $this->canonicalize($date_created);
if($this->validator->isValidDate("Content date created", $date_created, "Y-m-d H:i:s", false)) {
$this->date_created = $date_created;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function clear_error_list() {
$this->error_list = null;
}
function get_error_list() {
return $this->error_list;
}
private function retrieve_content($content_id) {
$db = DB::get_instance();
$sql = "SELECT * FROM content WHERE id = " . $content_id;
$result = $db->query($sql);
$row = $db->fetch_assoc($result);
$this->content_id = $row['id'];
$this->user_id = $row['user_id'];
$this->title = $row['title'];
$this->content = $row['content'];
$this->date_created = $row['date_created'];
}
function write() {
$db = DB::get_instance();
$sql = "INSERT INTO content (user_id, title, content, date_created) values ('" . $this->user_id . "', '" . $this->title . "', '" . $this->content . "', '" . date("Y-m-d") . "')";
$result = $db->query($sql);
}
function canonicalize($input) {
try {
$input = $this->encoder->canonicalize($input);
} catch (IntrusionException $e) {
echo($e->getUserMessage());
exit();
}
return $input;
}
}
?>
Let's put our Validator to good use and edit the controllers where we call these functions to deal with the errors. Lucky for us, that all takes place in one file, post.php. Because we've put so much of this into our classes, it only takes the addition of a few lines of code in our controller to implement our changes. Our new post.php looks like this:
<?php
/**
*
* post.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");
$db = DB::get_instance();
$ESAPI = new ESAPI("/var/www/insecure_week2/lib/owasp/test/testresources/ESAPI.xml");
if($_POST['submit']) {
$content = new Content();
$content->set_user_id($_POST['user_id']);
$content->set_title($_POST['title']);
$content->set_content($_POST['content']);
$error_list = $content->get_error_list();
if(!count($error_list)) {
$content->write();
header("Location:index.php");
}
}
$user = new User();
include("post.html");
if($error_list) {
$content->clear_error_list();
}
?>
And we'll edit post.html to display a list of the errors. Again, we're just adding a few easy lines of code.
<html>
<head>
<title>Post</title>
</head>
<body>
<?php if($error_list) {
for($i=0;$i<count($error_list);$i++) {
?>
<font color="red"><?= $error_list[$i] ?></font><br>
<?php } } ?>
Post new content<br>
<form name="content" method="post" action="post.php">
Title: <input type="text" name="title"><br>
Content: <textarea name="content"></textarea><br>
<input type="hidden" name="user_id" value="<?= $user->get_user_id() ?>">
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
We'll do the same thing with our Comment class/controller/html. In our Comment class, we're going to do parameter validation, create an error list, and improve our constructor a little bit. This should all pretty much make sense if you read the Content class portion. The new Comment class looks like this:
<?php
/**
*
* Comment.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require_once("owasp/src/ESAPI.php");
require_once("owasp/src/reference/BlogValidator.php");
class Comment {
private $comment_id = null;
private $comment = null;
private $content_id = null;
private $date_created = null;
private $esapi = null;
private $encoder = null;
private $validator = null;
private $error_list = null;
function __construct($comment_id = '') {
$this->esapi = new ESAPI("/var/www/insecure_week2/lib/owasp/test/testresources/ESAPI.xml");
ESAPI::setEncoder(new DefaultEncoder());
ESAPI::setValidator(new BlogValidator());
$this->encoder = ESAPI::getEncoder();
$this->validator = ESAPI::getValidator();
if($comment_id) {
$this->retrieve_comment($comment_id);
}
}
function get_comment_id() {
return $this->comment_id;
}
function set_comment_id($comment_id) {
$comment_id = $this->canonicalize($comment_id);
if($this->validator->isValidNumber("Comment ID", $comment_id, 1, 65535, false)) {
$this->comment_id = $comment_id;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function get_comment() {
return $this->comment;
}
function set_comment($comment) {
$comment = $this->canonicalize($comment);
if($this->validator->isValidPrintable("Comment", $comment, 140, false)) {
$this->comment= $comment;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function get_content_id() {
return $this->content_id;
}
function set_content_id($content_id) {
$content_id = $this->canonicalize($content_id);
if($this->validator->isValidNumber("Content ID", $content_id, 1, 65535, false)) {
$this->content_id = $content_id;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function get_date_created() {
return $this->date_created;
}
function set_date_created($date_created) {
$date->created = $this->canonicalize($date_created);
if($this->validator->isValidDate("Content date created", $date_created, "Y-m-d H:i:s", false)) {
$this->date_created = $date_created;
} else {
$this->error_list[] = $this->validator->getLastError();
}
}
function write() {
$db = DB::get_instance();
$sql = "insert into comments (comment, content_id, date_created) values ('" . $this->comment . "', '" . $this->content_id . "', '" . date("Y-m-d") . "')";
$result = $db->query($sql);
}
function canonicalize($input) {
try {
$input = $this->encoder->canonicalize($input);
} catch (IntrusionException $e) {
echo($e->getUserMessage());
exit();
}
return $input;
}
function clear_error_list() {
$this->error_list = null;
}
function get_error_list() {
return $this->error_list;
}
function retrieve_comment($comment_id) {
$db = DB::get_instance();
$sql = "SELECT * FROM comments WHERE id = " . $comment_id;
$result = $db->query($sql);
$row = $db->fetch_assoc($result);
$this->comment_id = $row['comment_id'];
$this->comment = $row['comment'];
$this->content_id = $row['content_id'];
$this->date_created = $row['date_created'];
}
}
?>
Our new comment.php controller looks like this:
<?php
/**
*
* comment.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");
$db = DB::get_instance();
$ESAPI = new ESAPI("lib/owasp/test/testresources/ESAPI.xml");
if($_POST['submit']) {
//save the comment
$comment = new Comment();
$comment->set_comment($_POST['comment']);
$comment->set_content_id($_POST['content_id']);
$error_list = $comment->get_error_list();
if(!count($error_list)) {
$comment->write();
header("Location:index.php");
}
}
$content_id = $_GET['content_id'];
if(!$content_id) {
$content_id = $_POST['content_id'];
}
$content = new Content($content_id);
if(!$content_id) {
echo("Missing content id");
exit();
}
include("comment.html");
if($error_list) {
$content->clear_error_list();
}
And it's html with the error list looks like:
<html>
<head>
<title>Comment</title>
</head>
<body>
<?php if($error_list) {
for($i=0;$i<count($error_list);$i++) {
?>
<font color="red"><?= $error_list[$i] ?></font><br>
<?php } } ?>
Comment on <?= $content->get_title() ?>
<form name="comment" method="post" action="comment.php">
Comment:<textarea name="comment"></textarea>
<input type="hidden" name="content_id" value="<?= $content_id ?>">
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
I think we've done quite a bit for this week. Next week, we're going to go into detail about how to handle our output and create safe database queries. You can find a link to the current project here - I didn't include the entire ESAPI PHP project, just our customized files, DefaultSecurityConfiguration.php and BlogValidator.php. They're in the main directory, you can move them into place after you check out the ESAPI PHP code.
As always, I'd love to hear feedback on this post. You can reach me at @jackwillk on Twitter, jackwillksecurity at gmail dot com, or leave a comment at the link below.
Wednesday, June 23, 2010
Using the OWASP PHP ESAPI - Part 1
OK, my first blog post! To kick this off, I'm going to do a series of posts about the Open Web Application Security Project (OWASP) PHP ESAPI. The OWASP PHP ESAPI is a free, open source, web application security control library that makes it easier for programmers to write lower-risk applications (taken straight from OWASP's site). It's fairly easy to use, and a great educational resource if you're willing to explore the source code a bit. Ideally, even if you're somewhat new to PHP, you should be able to follow this and learn a thing or two about securing a PHP web application.
Over the next three weeks, we're going to talk a bit about application security and what the PHP ESAPI is and is not.
Week 1: We will create a simple PHP blog application rife with security vulnerabilities. This is the sort of application you will find in many tutorials and beginner PHP books.
Week 2: We'll discuss some of these vulnerabilities and retrofit our application with the OWASP PHP ESAPI.
Week 3: The ESAPI is great and all, but there are some things that it just can't protect against, like logic errors. This week, we'll discuss the logic problems in our blog application and the limits of the ESAPI.
Just a note before we get started: I'll be demonstrating some very insecure code to start this series out. It would likely be a very big mistake to use this in any kind of of live environment. So with that said, let's get going.
We're going to start by building a simple, insecure PHP blogging application. We're not really interested in building something awesome, just something with enough of an attack surface to demonstrate some good use of the ESAPI. Our application will allow an admin user to post content to a page and update that content. Anyone else is allowed to comment on that content. Revolutionary, huh?
We can see from our description, we'll need a few different components:
1. A piece that will display the content and comments
2. A piece that will allow an authenticated user to enter content
3. A login piece that will allow a user to log in
4. A piece that will allow a visitor to enter a comment
We'll start by creating a simple database to house our application data. I'm using MySQL.
Next, we'll create a few classes our application will use. A DB singleton class, a User class, a Comment class and a Content class. First up, our DB class. I made this whole thing available at insecure_app.tar.gz to make it easier to follow along at home.
As it mentions in the comments, this is just the barest wrapper for the built in MySQL functions. We'll be fixing this up later. Next, our User class.
This class basically handles the login and session stuff. I like to explicitly define the accessor methods instead of using PHP's built in magic functions for this. I feel that it makes the code cleaner and easier to read and we can also add validation into them later. But for now, we'll stick with making this app as vulnerable and possible, so no validation!
Now we'll build or Content class to handle the blog posts.
Standard class with create and read functions. Our Comment class is similar:
And that does it for our classes, easy huh? Now we just need a controller for each of our pages and our HTML. The first page we'll deal with is our main blog page.
And our HTML file looks like:
And our login page:
And its html
The page where we'll post to our blog
And its HTML
And finally, our comment page
And its HTML
And there you have it! One extremely insecure blog application. I think that's enough for my first blog post! Next week, we'll begin discussing what's wrong with this mess and how to secure it using the OWASP PHP ESAPI.
I am pretty new at this whole blogging thing, and I would love to hear some feedback. So feel free to leave a comment, send me an email (jackwillksecurity at gmail dot com) or reach out to me on Twitter (@jackwillk)
Over the next three weeks, we're going to talk a bit about application security and what the PHP ESAPI is and is not.
Week 1: We will create a simple PHP blog application rife with security vulnerabilities. This is the sort of application you will find in many tutorials and beginner PHP books.
Week 2: We'll discuss some of these vulnerabilities and retrofit our application with the OWASP PHP ESAPI.
Week 3: The ESAPI is great and all, but there are some things that it just can't protect against, like logic errors. This week, we'll discuss the logic problems in our blog application and the limits of the ESAPI.
Just a note before we get started: I'll be demonstrating some very insecure code to start this series out. It would likely be a very big mistake to use this in any kind of of live environment. So with that said, let's get going.
We're going to start by building a simple, insecure PHP blogging application. We're not really interested in building something awesome, just something with enough of an attack surface to demonstrate some good use of the ESAPI. Our application will allow an admin user to post content to a page and update that content. Anyone else is allowed to comment on that content. Revolutionary, huh?
We can see from our description, we'll need a few different components:
1. A piece that will display the content and comments
2. A piece that will allow an authenticated user to enter content
3. A login piece that will allow a user to log in
4. A piece that will allow a visitor to enter a comment
We'll start by creating a simple database to house our application data. I'm using MySQL.
USE insecure;
CREATE TABLE user (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(16), password VARCHAR(16));
CREATE TABLE content (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, title VARCHAR(140), content TEXT, date_created TIMESTAMP);
CREATE TABLE comments (id INT AUTO_INCREMENT PRIMARY KEY, comment VARCHAR(140), content_id INT, date_created TIMESTAMP);
Next, we'll create a few classes our application will use. A DB singleton class, a User class, a Comment class and a Content class. First up, our DB class. I made this whole thing available at insecure_app.tar.gz to make it easier to follow along at home.
<?php
/**
*
* DB.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment.
*
* @author jackwillk
* @created 2010
*
*/
class DB {
/**
* This class provides the barest wrapper for MySQL
*/
private $host = "localhost";
private $username = "root";
private $password = "supersecretpw";
private $db_name = "insecure";
private static $instance;
private function __construct() {
$this->connect();
}
function connect() {
$this->connection = mysql_connect($this->host, $this->username, $this->password);
if(!$this->connection) {
echo(mysql_error());
}
mysql_select_db($this->db_name);
}
function query($sql) {
$result = mysql_query($sql, $this->connection);
if(!$result) {
echo(mysql_error());
}
return $result;
}
function count_rows($result) {
return mysql_num_rows($result);
}
function fetch_assoc($result) {
return mysql_fetch_assoc($result);
}
static function get_instance() {
if(!self::$instance) {
self::$instance = new DB;
}
return self::$instance;
}
}
?>
As it mentions in the comments, this is just the barest wrapper for the built in MySQL functions. We'll be fixing this up later. Next, our User class.
<?php
/**
*
* User.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
class User {
private $username = null;
private $password = null;
private $user_id = null;
function __construct($user_id='') {
if($this->check_user_session()) {
$this->set_username($_SESSION['username']);
$this->set_user_id($_SESSION['user_id']);
}
}
function get_user_id() {
return $this->user_id;
}
function set_user_id($user_id) {
$this->user_id = $user_id;
}
function get_username() {
return $this->username;
}
function set_username($username) {
$this->username = $username;
}
/**
* This function will check a user submitted username and password against
* the database. If it exists, it sets up a new session
*/
function login($username, $password) {
$db = DB::get_instance();
$sql = "select * from user where username = '$username' and password = '$password'";
$result= $db->query($sql);
if($db->count_rows($result)) {
$row = $db->fetch_assoc($result);
$this->set_user_id($row['id']);
$this->set_username($row['username']);
$this->create_user_session();
}
}
/**
* This function is used to start a session and set initial variables
*/
private function create_user_session() {
session_start();
$_SESSION['user_id'] = $this->get_user_id();
$_SESSION['username'] = $this->get_username();
}
/**
* This function checks to see if a user is logged in.
*/
function check_user_session() {
session_start();
if($_SESSION['user_id'] && $_SESSION['username']) {
return true;
}
return false;
}
}
?>
This class basically handles the login and session stuff. I like to explicitly define the accessor methods instead of using PHP's built in magic functions for this. I feel that it makes the code cleaner and easier to read and we can also add validation into them later. But for now, we'll stick with making this app as vulnerable and possible, so no validation!
Now we'll build or Content class to handle the blog posts.
<?php
/**
*
* Content.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
class Content {
private $content_id = null;
private $user_id = null;
private $title = null;
private $content = null;
private $date_created = null;
function __construct($content_id = '', $user_id = '', $title = '', $content = '', $date_created = '') {
if($content_id) {
$this->content_id = $content_id;
$this->retrieve_content();
} else {
$this->user_id = $user_id;
$this->title = $title;
$this->content = $content;
$this->date_created = $date_created;
}
}
function get_content_id() {
return $this->content_id;
}
function set_content_id($content_id) {
$this->content_id = $content_id;
}
function get_user_id() {
return $this->user_id;
}
function set_user_id($user_id) {
$this->user_id = $user_id;
}
function get_title() {
return $this->title;
}
function set_title($title) {
$this->title = $title;
}
function get_content() {
return $this->content;
}
function set_content($content) {
$this->content = $content;
}
function get_date_created() {
return $this->date_created();
}
function set_date_created($date_created) {
$this->date_created = $date_created;
}
private function retrieve_content() {
$db = DB::get_instance();
$sql = "SELECT * FROM content WHERE id = " . $this->content_id;
$result = $db->query($sql);
$row = $db->fetch_assoc($result);
$this->__construct('', $row['user_id'], $row['title'], $row['content'], $row['date_created']);
}
function write() {
$db = DB::get_instance();
$sql = "INSERT INTO content (user_id, title, content, date_created) values ('" . $this->user_id . "', '" . $this->title . "', '" . $this->content . "', '" . date("Y-m-d") . "')";
$result = $db->query($sql);
}
}
?>
Standard class with create and read functions. Our Comment class is similar:
<?php
/**
*
* Comment.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
class Comment {
private $comment_id = null;
private $comment = null;
private $content_id = null;
private $date_created = null;
function __construct($comment_id='', $comment='', $content_id='', $date_created='') {
$this->set_comment_id($comment_id);
$this->set_comment($comment);
$this->set_content_id($content_id);
$this->set_date_created($date_created);
}
function get_comment_id() {
return $this->comment_id;
}
function set_comment_id($comment_id) {
$this->comment_id = $comment_id;
}
function get_comment() {
return $this->comment;
}
function set_comment($comment) {
$this->comment = $comment;
}
function get_content_id() {
return $this->content_id;
}
function set_content_id($content_id) {
$this->content_id = $content_id;
}
function get_date_created() {
return $this->date_created;
}
function set_date_created($date_created) {
$this->date_created = $date_created;
}
function write() {
$db = DB::get_instance();
$sql = "insert into comments (comment, content_id, date_created) values ('" . $this->comment . "', '" . $this->content_id . "', '" . date("Y-m-d") . "')";
$result = $db->query($sql);
}
}
?>
And that does it for our classes, easy huh? Now we just need a controller for each of our pages and our HTML. The first page we'll deal with is our main blog page.
<?php
/**
*
* index.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");
$db = DB::get_instance();
function get_all_content() {
$db = DB::get_instance();
$sql = "SELECT * FROM content order by date_created";
$result = $db->query($sql);
while($row=$db->fetch_assoc($result)) {
$content_arr[] = new Content($row['id'], $row['user_id'], $row['title'], $row['content'], $row['date_created']);
}
return $content_arr;
}
function get_all_comments($content_id) {
$db = DB::get_instance();
$sql = "SELECT * FROM comments where content_id = '$content_id' order by date_created";
$result = $db->query($sql);
while($row = $db->fetch_assoc($result)) {
$comment_arr[] = new Comment($row['id'], $row['comment'], $row['content_id'], $row['date_created']);
}
return $comment_arr;
}
function construct_content_display($content_arr) {
$output = '';
for($i=0;$i<count($content_arr);$i++) {
$output .= "<p><b>" . $content_arr[$i]->get_title() . "</b></p>";
$output .= "<br><p>" . $content_arr[$i]->get_content() . "</p>";
$comment_arr = get_all_comments($content_arr[$i]->get_content_id());
for($j=0;$j<count($comment_arr);$j++) {
$output .= "<br><p>" . $comment_arr[$j]->get_comment() . " - " . $comment_arr[$j]->get_date_created() . "</p>";
}
$output .= "<br><a href=\"comment.php?content_id=" . $content_arr[$i]->get_content_id() . "\">Comment</a>";
}
return $output;
}
$user = new User();
$content_arr = get_all_content();
include("index.html");
?>
And our HTML file looks like:
<html>
<head>
<title>Insecure!</title>
</head>
<body>
<a href="login.php">Login</a><br>
<?php if($user->get_user_id()) { ?>
<a href="post.php">Post New Content</a><br><br>
<?php } ?>
<?php echo(construct_content_display($content_arr)); ?>
</body>
</html>
And our login page:
<?php
/**
*
* login.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require("lib/DB.php");
require("lib/User.php");
$db = DB::get_instance();
$user = new User();
if($user->get_user_id()) {
echo("You are already logged in.");
exit();
}
if($_POST['submit']) {
$user->login($_POST['username'], $_POST['password']);
if($user->get_user_id()) {
header("Location:index.php");
} else {
echo("Username or password was incorrect<br>");
}
}
include("login.html");
?>
And its html
<html>
<head>
<title>Login</title>
</head>
<body>
<b>This is my awesome login page!</b>
<form name="login" method="post" action="login.php">
username:<input type="text" name="username"><br>
password:<input type="text" name="password"><br>
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
The page where we'll post to our blog
<?php
/**
*
* post.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");
$db = DB::get_instance();
if($_POST['submit']) {
$content = new Content();
$content->set_user_id($_POST['user_id']);
$content->set_title($_POST['title']);
$content->set_content($_POST['content']);
$content->write();
header("Location:index.php");
}
$user = new User();
include("post.html");
?>
And its HTML
<html>
<head>
<title>Post</title>
</head>
<body>
Post new content<br>
<form name="content" method="post" action="post.php">
Title: <input type="text" name="title"><br>
Content: <textarea name="content"></textarea><br>
<input type="hidden" name="user_id" value="<?= $user->get_user_id() ?>">
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
And finally, our comment page
<?php
/**
*
* comment.php
*
* This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
* this in any kind of production environment
*
* @author jackwillk
* @created 2010
*
*/
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");
$db = DB::get_instance();
if($_POST['submit']) {
//save the comment
$comment = new Comment();
$comment->set_comment($_POST['comment']);
$comment->set_content_id($_POST['content_id']);
$comment->write();
header("Location:index.php");
}
$content_id = $_GET['content_id'];
if(!$content_id) {
echo("Missing content id");
exit();
}
include("comment.html");
And its HTML
<html>
<head>
<title>Comment</title>
</head>
<body>
Comment on <?= $content->get_title ?>
<form name="comment" method="post" action="comment.php">
Comment:<textarea name="comment"></textarea>
<input type="hidden" name="content_id" value="<?= $content_id ?>">
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
And there you have it! One extremely insecure blog application. I think that's enough for my first blog post! Next week, we'll begin discussing what's wrong with this mess and how to secure it using the OWASP PHP ESAPI.
I am pretty new at this whole blogging thing, and I would love to hear some feedback. So feel free to leave a comment, send me an email (jackwillksecurity at gmail dot com) or reach out to me on Twitter (@jackwillk)
Subscribe to:
Posts (Atom)