For a while, Microsoft have had a flag for cookies called ‘httponly’. This doesn’t sound particularly exciting, but it is a vital step forward for web application security.
This flag tells Internet Explorer to make this cookie ‘invisible’ to javascript (and other scripting languages) which means that an XSS attack will no longer be able to steal your sensitive cookies.
The problem is that ‘http only’ support has only just been added into PHP 5.2. This makes this feature unavailable to most webservers.
However, there appears to be a way to force this flag to be written regardless of your PHP version by simply adding “; HttpOnly” at the end of the domain name when setting the cookie. PHP’s “setcookie” function merely formats the data into a “set-cookie” header. Fortunately, PHP doesn’t appear to filter out or escape the semi-colon so it’s added to the end of the “set-cookie” request.
if ( PHP_VERSION < 5.2 )
{
@setcookie( $name, $value, $expires, $path, $domain. '; HttpOnly' );
}
else
{
@setcookie( $name, $value, $expires, $path, $domain, NULL, TRUE );
}
I've tested this out and it appears to work fine. IE7 shows the "sensitive" cookie data in the document.cookie string without adding the flag. Adding the flag onto the domain string causes the sensitive cookies to disappear from the document.cookie string.
Firefox ignores it and sets cookies as does Safari and Opera. I'll do some more testing and report in on my findings. I also have a Firefox friendly version to stop access to the document.cookie which I'll post up tomorrow.
UPDATE 14th September
I've downloaded the source to PHP 5 to make confirm that this 'hack' will work across different platforms. The source code confirms that no cleaning takes place on the domain name attribute (or indeed any other than the cookie name and value).
These snippets are from the head.c document, function php_set_cookie:
if (name && strpbrk(name, "=,; \t\r\n\013\014") != NULL) { /* man isspace for \013 and \014 */
zend_error( E_WARNING, "Cookie names can not contain any of the folllowing '=,; \t\r\n\013\014' (%s)", name );
return FAILURE;
}
if (!url_encode && value && strpbrk(value, ",; \t\r\n\013\014") != NULL) { /* man isspace for \013 and \014 */
zend_error( E_WARNING, "Cookie values can not contain any of the folllowing ',; \t\r\n\013\014' (%s)", value );
return FAILURE;
}
That shows the check for the key and value:
if (path && path_len > 0) {
strcat(cookie, "; path=");
strcat(cookie, path);
}
if (domain && domain_len > 0) {
strcat(cookie, "; domain=");
strcat(cookie, domain);
}
That shows that no cleaning takes place and this 'hack' will execute perfectly.
{ 11 comments… read them below or add one }
Nice; I’ll be using this.
Thanks Matt. I actually was recently just looking into something similar. I appreciate the info.
HttpOnly cookies are a great system that we considered should be more accessible to all web application developers.
When we integrated HttpOnly cookies into vBulletin 3.6.1 a few weeks ago, we were disappointed to see that PHP did not natively support them through the setcookie() function, so we submitted a patch to PHP, which was accepted.
Glad you appreciate our efforts!
I know that it was Scott that wrote the little patch for PHP.
The great thing is that you don’t need it — or at very least, don’t need to wait for PHP 5.2
I’m going to post up my Firefox version later today that you may want to consider using too.
In the absense of PHP 5.2 and its native HttpOnly support, we are actually rolling our own cookies using header() calls, as you may have seen. Your hack of the setcookie() function is nice, I’m surprised PHP doesn’t clean for the semi-colon, but there we go.
I’ll be interested to see how you’ve tackled the Firefox issue.
I was surprised too. I can only assume they didn’t consider they needed to clean the domain name as no valid domain name allows for a semi-colon.
This reminds me, it would be nice to be able to send custom fields to the setcookie() function like one can with mail() – it would future proof PHP against changes in cookie syntax.
I’ve updated my original blog post. I’ve had a dig around in the PHP source.
Matt, you should use version_compare(). When PHP_VERSION is ‘5.2.1′, the comparsion PHP_VERSION < 5.2 returns FALSE.
ups… my mistake. PHP_VERSION must be something like ‘5.10.1′ to see difference.
@Matt: thanks for the tips, I just applied your suggestion to my IPB’s setcookie function, as it will take months to see PHP 5.2 coming.