Edd Mann Developer

Securing Sessions in PHP

Following on from my previous post on Self-signed SSL certificates, I would now like to address the second most common Web application vulnerability (Broken Authentication and Session Management). When delving into the subject I was unable to find a definitive resource for an PHP implementation. Due to this, I set out to combine all the best practice I could find into a single Session handler, to help protect against the common attack vectors. Since PHP 5.4, you are able to set the Session handler based on a class instance that extends the default ‘SessionHandler’ class.

In my initial research I found multiple commonly mis-configured options that should be addressed, these are presented below in the ‘setup’ method. Here we specify that sessions should only be passed via cookies, removing the possibility of the session identifier being sent as a ‘GET’ parameter. We then alter the session name (from the default) to a specified application-specific name, which is good practice. Finally, we set the cookie parameters of the session identifier. These parameters can be overridden when initialising the session handler, however, recommended defaults of only allowing it to be sent over HTTPS (if present) and restricted HTTP access (no client-side script access). It is recommended that you override the path and domain based on the application instance (abiding by the principle of least privilege).

class SecureSessionHandler extends SessionHandler {

    protected $key, $name, $cookie;

    public function __construct($key, $name = 'MY_SESSION', $cookie = [])
    {
        $this->key = $key;
        $this->name = $name;
        $this->cookie = $cookie;

        $this->cookie += [
            'lifetime' => 0,
            'path'     => ini_get('session.cookie_path'),
            'domain'   => ini_get('session.cookie_domain'),
            'secure'   => isset($_SERVER['HTTPS']),
            'httponly' => true
        ];

        $this->setup();
    }

    protected function setup()
    {
        ini_set('session.use_cookies', 1);
        ini_set('session.use_only_cookies', 1);

        session_name($this->name);

        session_set_cookie_params(
            $this->cookie['lifetime'], $this->cookie['path'],
            $this->cookie['domain'], $this->cookie['secure'],
            $this->cookie['httponly']
        );
    }

    // ...

}

Session Management

With the environment successfully configured we are now able to safety start, destroy and regenerate the current session using the provided methods below. The ‘start’ method in-principle wraps the ‘session_start’ function, however, as a precaution there is a one-in-five chance that the session identifier is regenerated (to address session fixation). The ‘forget’ method removes the contents of the ‘$_SESSION’ array (for access during the remainder of the current request), expires the session cookie and then destroys the session itself. Finally, the ‘refresh’ method replaces the current session identifier with a new one.

public function start()
{
    if (session_id() === '') {
        if (session_start()) {
            return (mt_rand(0, 4) === 0) ? $this->refresh() : true; // 1/5
        }
    }

    return false;
}

public function forget()
{
    if (session_id() === '') {
        return false;
    }

    $_SESSION = [];

    setcookie(
        $this->name, '', time() - 42000,
        $this->cookie['path'], $this->cookie['domain'],
        $this->cookie['secure'], $this->cookie['httponly']
    );

    return session_destroy();
}

public function refresh()
{
    return session_regenerate_id(true);
}

Session Content Encryption

There are many different ways to persist the contents of a user’s session, dependent on your business requirements. Extending the default SessionHandler allows us to rewrite how sessions are read and written. In this case I have decided to encrypt/decrypt (using mcrypt) the serialized contents using a specified key, before calling the original read/write methods. Typically the session contents are written to plain-text files, and if not correctly configured (file permissions, directory location) are stored in the global PHP session directory. This may not be an issue if you have sole access to the server (or are using a security patch such as Suhosin), but in a shared-hosting environment could result in a major security breach.

public function read($id)
{
    return mcrypt_decrypt(MCRYPT_3DES, $this->key, parent::read($id), MCRYPT_MODE_ECB);
}

public function write($id, $data)
{
    return parent::write($id, mcrypt_encrypt(MCRYPT_3DES, $this->key, $data, MCRYPT_MODE_ECB));
}

Session Expiration and Validation

With the session contents now encrypted we can move on to make sure that the current session timeouts (expires) after a defined idle period, and the user is as expected (to counter session hijacking). Sessions are garbage collected based on a configured time, however, it is good practice to check and expire idle sessions in the application-land per-request. The example below stores the time of the last activity in the user’s session - checking against the current time to validate if it has exceeded the specified duration (in minutes). Fingerprinting the current session with the user’s user-agent and IP address allows us to provide another layer of security against session hijacking. To address network proxy issues (which could cause false positives), only the first two IP blocks are used in the calculation of the fingerprint hash.

public function isExpired($ttl = 30)
{
    $activity = isset($_SESSION['_last_activity'])
        ? $_SESSION['_last_activity']
        : false;

    if ($activity !== false && time() - $activity > $ttl * 60) {
        return true;
    }

    $_SESSION['_last_activity'] = time();

    return false;
}

public function isFingerprint()
{
    $hash = md5(
        $_SERVER['HTTP_USER_AGENT'] .
        (ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0'))
    );

    if (isset($_SESSION['_fingerprint'])) {
        return $_SESSION['_fingerprint'] === $hash;
    }

    $_SESSION['_fingerprint'] = $hash;

    return true;
}

public function isValid($ttl = 30)
{
    return ! $this->isExpired($ttl) && $this->isFingerprint();
}

Session Access

This section is not exactly security related, but as we are creating a session wrapper, it would be worthwhile to provide the ability to add and retrieve session values based on dot notation. Doing so increases the readability when accessing large array based data-structures.

public function get($name)
{
    $parsed = explode('.', $name);

    $result = $_SESSION;

    while ($parsed) {
        $next = array_shift($parsed);

        if (isset($result[$next])) {
            $result = $result[$next];
        } else {
            return null;
        }
    }

    return $result;
}

public function put($name, $value)
{
    $parsed = explode('.', $name);

    $session =& $_SESSION;

    while (count($parsed) > 1) {
        $next = array_shift($parsed);

        if ( ! isset($session[$next]) || ! is_array($session[$next])) {
            $session[$next] = [];
        }

        $session =& $session[$next];
    }

    $session[array_shift($parsed)] = $value;
}

Example Usage

With the implementation now in place we can see the example in-practice. I have constructed a test environment which makes sure that the PHP configuration uses session files, stored in a relative directory. In conjunction, we create a new session handler instance (passing in our encryption key) and register it with the PHP engine.

$session = new SecureSessionHandler('cheese');

ini_set('session.save_handler', 'files');
session_set_save_handler($session, true);
session_save_path(__DIR__ . '/sessions');

Finally, we start the session instance and add a worthwhile validation check (expiration of five minutes), deleting the session if the checks do not pass. Values can then be added to the current session using the ‘put’ method and accessed in a similar manner.

$session->start();

if ( ! $session->isValid(5)) {
    $session->destroy();
}

$session->put('hello.world', 'bonjour');

$session->get('hello.world'); // bonjour

The full class implementation can be found as a Gist - SecureSessionHandler.php.

Resources