Erik Sasha Romijn

The definitive guide to cookie domains and why a www-prefix makes your website safer

Restricting access to cookies is essential for security in many web apps. For example, the session ID, the secret token used to identify a particular session, is typically stored in a cookie. Cookies have several important settings. Previously, I discussed the secure flag. This time, let’s dive into the cookie domain.

The cookie domain is an important security feature, probably even more important than the secure flag. It tells the browser that this cookie must only be sent to matching domains. Matching however, can happen in several ways. Perhaps domain is a bit of a misnomer: this can be any host name, like foobar.erik.io.

With this in mind, I did some digging into the exact workings of cookie domains, and was surprised to find this less straight forward than I had expected. And, it turns out Internet Explorer’s RFC-incompliant behaviour makes it safer to host your websites with a www-prefix, so www.erik.io instead of erik.io.

Update: this post was updated on April 9, 2014, to reflect that Internet Explorer misbehaves with domain-less cookies, as learned from this blog post. Previously, I concluded that a www-prefix makes no difference, with this new knowledge, a www-prefix is safer.

Key points

  • When not setting an explicit domain for a cookie, the default in most browsers is to only send the cookie when the domain matches exactly. However, Internet Explorer violates the RFC, and will send it to all subdomains as well.
  • The most compatible way of having a cookie visible to this domain and all sub domains, is to prefix it with a dot, like .example.com for example.com and all sub domains (and their sub domains, etc.)
  • If you set a cookie domain without the dot prefix, like erik.io, this will still be treated as “erik.io and all sub domains”. Additionally, this is invalid in very old implementations.
  • Setting an explicit cookie domain may therefore actually decrease security, as you will now include all sub domains in all browsers. For Internet Explorer, there is no difference.
  • To keep your cookies safe, host your websites with a www-prefix, so www.erik.io instead of erik.io, as in the latter case, Internet Explorer will also send the cookies to any (perhaps malicious) subdomain of erik.io (in violation of the RFC).
  • Regardless of your cookie domains, if you place or modify sensitive data in sessions, make sure to rotate the session ID to prevent fixation attacks.
  • None of this excuses you of the responsibility to keep your domain clean from untrusted hosts. If you want to host untrusted user content, place it under a different domain.

I have tested this behaviour with the latest Firefox, Safari and Chrome under Mac OS X.

Domain matching specification

Cookies were defined in three RFCs, from 1997, 2000 and 2011, each succeeding the other.

RFC 2109 and RFC 2965

RFC 2965, published 2000, defines that exact matches are always considered a match, unsurprisingly. For partial matches, it states (where A is the request host name, and B is the cookie domain):

A is a HDN string and has the form NB, where N is a non-empty name string, B has the form .B’, and B’ is a HDN string. (So, x.y.com domain-matches .Y.com but not Y.com.)

In other words, if the cookie domain is erik.io, and the browser performs a request to foo.erik.io or www.erik.io, the cookie is not a match and will not be sent. If the cookie domain is .erik.io however, it will match.

However, it also specifies:

If an explicitly specified value does not start with a dot, the user agent supplies a leading dot.

So if you would set the cookie domain to erik.io in the header, the user agent should treat the cookie domain as .erik.io, and it will match with foo.erik.io.

Now, it may be that RFC 2965 was referring only to the Set-Cookie2 header. However, the older RFC 2109, from 1997, differs in only one part:

An explicitly specified domain must always start with a dot.

So in RFC 2109, setting a cookie domain to erik.io is simply invalid – you must set it to .erik.io for it to be valid, which means it will also match foo.erik.io.

However, this leaves an interesting question. What happens if no cookie domain was specified at all? Both state:

Domain: Defaults to the effective request-host. (Note that because there is no dot at the beginning of effective request-host, the default Domain can only domain-match itself.)

Therefore, in RFC 2109/2965 implementations, if a request is done to erik.io, and a cookie is set without an explicit domain, the domain defaults to erik.io – not to .erik.io! In that case, it will not match foo.erik.io. So a cookie set by a request to erik.io without an explicit domain, is very different compared to explicitly setting the erik.io domain in the cookie, as the latter is treated as .erik.io.

RFC 6265

The successor of RFC 2965, RFC 6265, was published in 2011. This means we will still encounter browsers that have not adopted to this specification. It has a somewhat different approach in section 5.1.3:

The domain string is a suffix of the string and the last character of the string that is not included in the domain string is a %x2E (“.”) character.

In other words, the erik.io cookie domain matches the foobar.erik.io request. So what about using .erik.io?

Note that a leading %x2E (“.”), if present, is ignored even though that character is not permitted

So .erik.io is identical to erik.io. However, there is another special condition:

If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.

So if a request is made to erik.io, a cookie is set without an explicit domain, the user agent must match it only to erik.io, and not to foobar.erik.io. The RFC notes that not all user agents might handle this correctly.

And then there is Internet Explorer

Unfortunately, this is only what the RFC specifies. Internet Explorer intentionally deviates from this (see Q3), although the reason for this is unknown to me.

In Internet Explorer, a cookie without an explicit domain is also sent to all subdomains. That deviation means that a cookie set without a domain from erik.io is more at risk than www.erik.io. In the former case, Internet Explorer will still send it to any subdomains of erik.io, in the latter case only to subdomains of www.erik.io, which will be less common. In the non-www case, just a single malicious host under that domain compromises all cookies you’ve set.

I built a simple test case for this scenario and users of Internet Explorer 9 and 11 reported that their browsers misbehaved.

Conclusion

Although the definitions are somewhat different, we can simplify it for any of these implementations as:

  • When no domain is set in the cookie, the cookie should only match the exact host name of the request. No sub domains, no partial matches. This means simply not including the domain attribute – it is not valid to set an empty domain attribute. Unfortunately, Internet Explorer appears to treat this as the host name along with any subdomains.
  • When setting a domain in the cookie, the safe choice is to have it preceded by a dot, like .erik.io. The cookie will match with all sub domains.
  • Setting a cookie domain without a preceding dot, like erik.io, is invalid in RFC 2109 implementations, and will produce the same behaviour as with a preceding dot on other implementations. There is no way to restrict a cookie to a specific explicitly set domain, without sub domains being included.

Other worthwhile observations:

  • In all RFCs, a specified cookie domain must match the current host name, per normal matching. Setting a cookie for www.erik.io in a response from erik.io is not valid, as a cookie with domain www.erik.io does not match erik.io, the former being more specific.
  • In RFC 6265, domains are explicitly lower cased when parsing the Set-Cookie header.

Malicious hosts under your domain

Although cookie domains do help to limit the scope of your cookies, it is still best to avoid having untrusted hosts under your domain. This is why GitHub pages are hosted under github.io, not github.com, for example. The risk is that any page under the same domain, can always place cookies at the domain-level.

One way to abuse this, which can not be fixed with any domain setting, is session fixation. Let’s say I control bad.example.com, and a Django site on example.com uses session cookies. I trick the victim into visiting bad.example.com, and set a sessionid cookie for .example.com. Remember, you can always set a cookie for a less specific domain, so this is allowed. I use a valid session ID, which I just got from the real example.com. Then, the victim visits example.com and the browser sends my valid sessionid cookie, because .example.com matches requests for example.com. The user logs in, and now their session, which I can access because I know the session ID, gives me access to their account.

Fortunately, this attack does not actually work in Django, because Django replaces the session ID when something significant happens, like a login or logout. At that point, the session ID I had is no longer useful. Remember to rotate the session ID as well for your own apps, when you add sensitive data to a session.

Shameless plug: an additional method of securing your sessions is my package django-restricted-sessions. In that case, the session would already have been reset as soon as the victim visits example.com and sends the planted sessionid cookie, unless the IP and user agent matches as well.

Demo

I hacked a quick demo of this on http://uname.nl/ and http://www.uname.nl/ and verified that this behaviour matches the latest Safari, Firefox and Chrome on Mac OS X. Each page sets a bunch of cookies with different domain settings, with and without prepended dots, and displays the visible cookies seen by the server side and javascript.

What about third party cookies?

Third party cookies are cookies placed for different domains than the page you are viewing. For example, if you visit erik.io, and that results in a cookie being set for example.com, we consider the latter a third party cookie.

Third party cookies are not prevented by domain matching, because the domain matching happens at the request level. A single page may result in many requests to many different domains. For each of those requests, the browser runs its cookie matching code to see which cookies to include. In the case I described, it might be that erik.io includes an image from example.com. When loading the image, the browser may receive or send cookies belonging to example.com, because that’s the host name for that particular request. If other pages also include an image hosted on example.com, the browser will send the cookies it received from previous requests. This is how third party cookies can be used for tracking across different websites.

Many browsers have an option to disable third party cookies. I haven’t looked into it, but logical way to do this, would be to add the restriction that cookies must match the domain of the URL of the page the user is viewing.

Further reading

From this blog, you may also like: