Session Handling in PHP

Traditionally HTTP is a stateless protocol. That is it is made up of requests and responses and there is no notion of a ‘persistent connection’. This means that there is no way to have consistency or personalisation on the web since there is no way of knowing whom the request came from.

There are 2 primary methods – regardless of platform – that are used as a work-around for this.

First is a cookie. A cookie is a text file that lives on the client’s computer that store values set by your application. In general the file can only be access by the website/domain that issued it. The main problem with cookies is that they are un-trusted. A malicious user could modify the data and cause you problems!

The second – and better – solution is sessions. Similar to cookies, but they reside on the server, and cannot be directly modified by the client. When sessions are used, a cookie is still issued, but this simply holds the ‘session id’. This session id is generally sent by any request after it is created by the app including actions such as form submission. This ‘session id’ is a link to either a file or record on the server – depending how they are stored.

As a session is the preferred method used by most developers, this is a short guide to get you started and outline some ‘best practices’ in using and securing them.

When using PHP, the very first thing you need to do before writing a single line of code is to ‘start’ your session.

A session must be started before headers are sent to the client, so the best approach is to do so at the VERY TOP of any page that you may want to access the session. This is done as so;

<?php
session_start();
… rest of page …
?>

Once a session has been started, you can start using session variables. Using session variables is similar to using an ‘associative array’. Session variable are accessed in the same way. Suppose we want to store the ‘username’ and the value of ‘johnsmith’, this would be done like so;

<?php
// Always Start the session
session_start();
// Assign the ‘session’ value of ‘username’ to ‘johnsmith’.
$_SESSION['username'] = 'johnsmith';
// Read the ‘session’ value of ‘username’ back to a local variable.
$username =  $_SESSION['username'];
echo $username;
?>

The above code would store, retrieve and then display the value of $_SESSION[‘username’]. Note: you do not need to assign a local variable, this is just an illustration of how to retrieve the value, echo $_SESSION[‘username’]; would have had the same effect.

A session and its variables are ‘persistent’ as long as the session is maintained (determined by a logout mechanism, exit browser, clearing cookies and session data from your browser, and by defined options in the php.ini file). The magic with sessions is that you can set values on one ‘page’ (or module/class) and then call it within any other page!

PHP by default stores the ‘session id’ in a cookie. Which – in my opinion – is the best place, storing and passing via form vales is unreliable and potentially more open to abuse. The cookie variable name can be changed via your ‘php.ini’ settings;

; Name of the session (used as cookie name).
session.name = PHPSESSID

You can change PHPSESSID to be anything you want if you wish.

However there is still a problem with sessions, the same as with cookies. Session Hijacking, and Session Fixation are common attacks that unscrupulous users may attempt to interfere with them.

Session Hijacking – this is when an attacker get the ‘session id’ of a valid user, they then spoof their ID to be that of the ‘victim’ – they have now assumed the identity of the ‘victim’.

Session fixation – almost the same. This time the attacker used a specially crafted URL or a forwarder to set the session ID. The user logs in, and the attacker used the ID he has to hijack the session.

I will now quickly discuss what we can do to protect against this.

Session Fixation

First lets demonstrate session fixation.

Create the following script, and save it as session.php;

<?php
session_start();
if (!isset($_SESSION['visits']))
{
    $_SESSION['visits'] = 1;
}
else
{
    $_SESSION['visits']++;
}
echo $_SESSION['visits'];
?>

On your first visit to the page, 1 should be output to the screen. On each subsequent visit, this should increment.

In order to demonstrate session fixation, make sure that you do not have an existing session identifier (perhaps delete your cookies), then visit this page with ?PHPSESSID=1234 appended to the URL. With a completely different browser (or even a completely different computer), visit the same URL again with ?PHPSESSID=1234 appended. Notice that you do not see 1 output on your first visit, but rather it continues the session you previously initiated.

Generally session fixation attacks use a link or a protocol-level redirect to send a user to a remote site with a session identifier appended to the URL. The user likely won’t notice, since the site will behave exactly the same. Because the attacker chose the session identifier, it is already known, and this can be used to launch impersonation attacks such as session hijacking.

An attack such as this is quite easy to prevent. If there isn’t an active session associated with a session identifier that the user is presenting, then regenerate it just to be sure:

<?php
session_start();
if (!isset($_SESSION['initiated']))
{
    session_regenerate_id();
    $_SESSION['initiated'] = true;
}
?>

The problem with such a simplistic defence is that an attacker can simply initialise a session for a particular session identifier, and then use that identifier to launch the attack.

To protect against this type of attack, it is only really useful after the user has logged in or otherwise gained privilege. If we regenerate the session id after any change in privilege (i.e after logging in and verifying the username/password), we will almost have eliminated the risk of a successful attack.

Session Hijacking

This is probably the most common session attack. This refers to all attacks than attempt to hijack or gain access to another user’s session.

If your session only consists of session_start(), you are vulnerable, although the exploit isn’t as simple.

For prevention of this, my approach is to complicate things for them, since complication increases security. For this we will look at the steps taken to hijack a session, we will assume the session id in each case has been compromised.

If simply using session_start() all that is required is a valid session id. To improve this we will make use of additional information in an HTTP request.

Note: IP addresses should not be relied upon as a single user could have a different IP for each request, and there can potentially be multiple users behind the same IP address.

A typical HTTP request:


GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0 Gecko
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234

Without boring you, the only thing required by HTTP/1.1 is the ‘host’ header. Therefore it seems odd to rely on anything else. But all we need id consistency since we only wish to complicate impersonation without affecting legitimate users.

So, imagine the previous request is followed by a request with a different User-Agent:


GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla Compatible (MSIE)
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234

While each request present the same cookie, it seems unlikely the user would have changed browsers (User-Agent) between the requests.

So, to the check we added previously we can enhance this to check the user agent;

<?php
session_start();
if (isset($_SESSION['HTTP_USER_AGENT']))
{
    if ($_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT']))
    {
        /* Prompt for password */
        exit;
    }
}
else
{
    $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
}
?>

Now, we need both a valid session id AND the correct browser (User-Agent). As intended this complicates things slightly and thus is more secure.

OK, so can this be made more secure? All along we have talked about getting a valid session id, so something additional is required still.

If we required the user to pass say an MD5 has of the User-Agent in each request, an attacker could no longer just recreate the headers of the requests, but would also need to pass this extra information. We can complicate their lives even further by adding some randomness to the way we construct the token:

<?php
$string = $_SERVER['HTTP_USER_AGENT'];
$string .= 'PROTECTME';
/* Add any other data that is consistent */
$fingerprint = md5($string);
?>

Now, keeping in mind the session id is in a cookie (which is vulnerable), we should pass this fingerprint as a URL variable. This should be in ALL URLs just like if it was the session id, since both should be required for a session to be continued – in addition to any other checks being passed.

If a check fails, simply prompt for a password so that real users are not treated like attackers! If your mechanism incorrectly suspects a user of impersonation, prompting for password is the least offensive way to proceed. Indeed, perceptive users may appreciate the extra protection this displays to them.

This is not a definitive way to complicate life for attackers. Hopefully this will have wet your appetite for protecting your app and that you will employ something in addition to a simple session_start(). Who knows, even come up with some ideas of your own to improve on it.

Just remember – make things difficult for baddies and easy for the good guys!

Note: Some claim that the User-Agent header is not consistent enough. The argument is that an HTTP proxy in a cluster can modify the User-Agent header inconsistently with other proxies in the same cluster. While I have never observed this myself (and feel comfortable relying on the consistency of User-Agent), it is something you may want to consider.

The Accept header has been known to change from request to request in Internet Explorer (depending on whether the user refreshes the browser), so this should not be relied upon for consistency.

Leave a Reply

Your email address will not be published. Required fields are marked *