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.