WEB Advent 2011 / Bake Cookies Like a Chef

It’s Christmas time again. While you write the first lines of your shiny new authentication library, the smell of tasty cookies is slowly sneaking into your room. The few minutes until they are finished is a good time to lean back and think about cookies — not the ones in the kitchen, but the cookies in your browser.

Let’s explore how to securely store data on the client side, and how to detect unwanted modifications.

Ingredients

Nearly every web app needs some kind of authentication, and most use cookies to help with identity. Cookies are a very convenient, portable, and scalable method to identify users after they have been authenticated. Unfortunately, you have to store those cookies on the client. The phrase “all user input is evil” is often used when you have to deal with data from the client, so it is important to realize that cookies are under the client’s control.

The first thing to remember is that cookies can provide more information to the user that you might think. For example, if your cookie contains a PHPSESSID key, an attacker doesn’t need fancy attack tools to immediately recognize that your app probably uses PHP. Every bit of information is important for an attacker, even if it doesn’t seem important.

An example from the Microsoft world is if you see ISAWPLB in a cookie, chances are that the target app uses the ISA Server as a reverse proxy or load balancer. Just look at the cookies of apps that you use regularly, and Google for some of the keys. Attackers can use this information in ways you would never consider. If you don’t believe me, take a look at the presentation entitled How I Met Your Girlfriend, by Samy Kamkar, which was presented at DEFCON 18. He shows you how to reduce the entropy of PHP sessions from 160 bits to 20 bits, which is much easier to brute force.

Attackers may also use cookies to bypass your authentication mechanisms. For example, if you implement a “remember me” feature by storing the user ID in the cookie (and no additional checks are performed on the server side), then the attacker may randomly change the ID in the cookie until he finds a valid one. It’s not very hard to guess that admin users have low IDs, right? If you want to learn more about attack vectors like this, I recommend you to start with the OWASP Top Ten Project where you can read about the top ten security risks in web apps, and how you can prevent them.

Does all of this mean that you should avoid cookies altogether? Of course not. Let’s take a look at two ways to encrypt your cookies and detect possible changes made to them. Note that the following code snippets are taken from the Lithium core. (Make sure to check it out if you haven’t already!) Additionally, I’ve modified the code snippets a bit to make them easier to understand. Lithium handles the following techniques transparently, so in practice, you only add and read data and don’t need to care about the implementation details.

Directions

A secure way to ensure that no one has tampered with your cookies is called HMAC (Hash-based Message Authentication Code). In a nutshell, your cookie gets signed with a hash that is generated from your data and a secret password. As a result, if the data changes, the hash will change, too.

PHP provides a handy hash_hmac() function that does the actual work for you. Here’s the code snippet from Lithium:

public static function signature($data, $secret = null) {
	unset($data['__signature']);
	$secret = ($secret) ?: static::$_secret;
	return hash_hmac('sha1', serialize($data), $secret);
}

It all boils down to calling hash_hmac() with an algorithm (in this case sha1), the actual data, and a secret password. Because you can only hash a string, you may need to serialize a non-scalar payload first. The signature will be appended to the payload, so you need to unset it first, so that the hash actually represents just the payload. It looks similar to this (let’s pretend that the static signature method is wrapped in a Cookie class and is public):

// password and payload
$secret = 'phpadvent';
$data = array('christmas' => 'fun');

// create the signature
$signature = Cookie::signature($data, $secret);

// store the cookie
$data += array('__signature' => $signature);
setcookie('mycookie', serialize($data));

To verify the cookie, remove the payload and generate the hash again. If the two hashes don’t match, Lithium raises a RuntimeException. This isn’t strictly necessary, but it makes sense in certain environments (you can then write exception handlers that log attack events, send emails, or block further requests).

public function read($currentData, array $options = array()) {
// ....

$currentSignature = $currentData['__signature'];
$signature = static::signature($currentData);

if ($signature !== $currentSignature) {
	$message = "Possible data tampering: HMAC signature does not match
data.";
	throw new RuntimeException($message);
}
return $data;
}

I recommend you read both the hash_hmac() documentation and the actual implementation in Lithium.

Message digests are a good way to detect data tampering, but in a lot of cases, it is an even better idea to encrypt all of your data. In order to do this, make sure to have the mcrypt extension installed. I recommend you use the AES algorithm in CBC mode (don’t use ECB, as it is considered far less secure).

The code for encryption is a bit more complicated, but it’s definitely worth the trouble.

protected static $_vector = null;

public function __construct($secret) {
	$this->_config = array(
		'secret' => $secret,
		'cipher' => MCRYPT_RIJNDAEL_256,
		'mode' => MCRYPT_MODE_CBC,
		'vector' => static::_vector(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC)
	);
}

protected static function _vector($cipher, $mode) {
	if (static::$_vector) {
		return static::$_vector;
	}

	$size = static::_vectorSize($cipher, $mode);
	return static::$_vector = mcrypt_create_iv($size, MCRYPT_DEV_URANDOM);
}

protected static function _vectorSize($cipher, $mode) {
	return mcrypt_get_iv_size($cipher, $mode);
}

public function encrypt($decrypted = array()) {
	$cipher = $this->_config['cipher'];
	$secret = $this->_config['secret'];
	$mode   = $this->_config['mode'];
	$vector = $this->_config['vector'];

	$encrypted = mcrypt_encrypt($cipher, $secret, serialize($decrypted),
$mode, $vector);
	$data = base64_encode($encrypted) . base64_encode($vector);

	return $data;
}

public function decrypt($encrypted) {
	$cipher = $this->_config['cipher'];
	$secret = $this->_config['secret'];
	$mode   = $this->_config['mode'];
	$vector = $this->_config['vector'];

	$vectorSize = strlen(base64_encode(str_repeat(" ",
static::_vectorSize($cipher, $mode))));
	$vector = base64_decode(substr($encrypted, -$vectorSize));
	$data = base64_decode(substr($encrypted, 0, -$vectorSize));

	$decrypted = mcrypt_decrypt($cipher, $secret, $data, $mode, $vector);
	$data = unserialize(trim($decrypted));

	return $data;
}

The actual work is done by the mcrypt_* methods. (Lithium provides internal wrappers.) The constructor (not shown here) creates a vector (by calling _vector()) and sets the cipher (MCRYPT_RIJNDAEL_256), the mode (MCRYPT_MODE_CBC), and a custom secret. When encrypt() is called, the unencrypted payload is serialized and encrypted with mcrypt_encrypt(). Because the encrypted data may contain incompatible characters for cookies, we need to base64_encode() it. Decrypting works in exactly the opposite way (decoding, decrypting, and unserializing). Here’s an example:

// password and payload
$secret = 'phpadvent';
$data = array('christmas' => 'fun');

// encrypting
$cookie = new Cookie($secret);
$encrypted = $cookie->encrypt($data);

/* Possible output:
* string(88)
"aAH84W30XJTRC1aFdvleJdv3H0Dzj/TPLVSYKyWe2yo=LtoWXG8ewGK6btLnO2OLGsOfTc6T97TLwwogmMXORHI="
*/
var_dump($encrypted);

// of course, we can decrypt it again
$decrypted = $cookie->decrypt($encrypted);

/* Output:
* array(1) { ["christmas"]=> string(3) "fun" }
*/
var_dump($decrypted);

If you are very careful, you can first add an HMAC signature to your payload and then encrypt it all.

Tasting

Now that we know the ingredients, lets bake secure cookies with Lithium:

use lithium\storage\Session;

Session::config(array('default' => array(
	'adapter' => 'Cookie',
	'strategies' => array('Encrypt' => array('secret' => 'p$hp#4dv3nTT'))
)));

Session::write('mykey', 'myvalue');

That was easy, right? Lithium provides you with a transparent abstraction to both session adapters (cookies and native PHP), and security strategies (HMAC and encryption), so you can mix them as you like. You can even write your own adapters (like a custom database adapter) and still profit from encryption strategies out of the box.

The key message I want you to take away is to take care of your cookies! Don’t store a single bit more than necessary on the client side, encrypt it, and make sure it was not modified outside of your control. PHP and the mcrypt extension provide you with everything you need, and good frameworks provide you with handy abstractions, so use them!

Developer Gift

Here’s a recipe for original viennese Vanillekipferl. You may need to convert the measurements into your own.

  • 300 grams of flour
  • 250 grams of butter (at room temperature)
  • 150 grams of ground hazelnuts
  • 100 grams of icing sugar
  • 1 teaspoon of vanilla sugar

Roast the hazelnuts in a pan without fat, and then let them cool. Mix all ingredients together in a bowl until they contain no chunks of flour and the dough is smooth. Afterwards, wrap the dough into wrapping film, and put it into the refrigerator for one hour.

Preheat the oven to 160°C. Chop the dough into small parts and form small Kipferl (U-Form, like seen in the Wikipedia article). Put them into the oven for about 15 minutes — they should only be slightly brown (not too dark!). While they are hot, turn them over into a mix of icing sugar and vanilla sugar so they are fully covered (like in the Wikipedia picture).

Let them rest for a few days in a box before serving!

Other posts