Cookies are tiny pieces of data attached to requests that your browser sends. Their most important use is for authentication so that a web server can know if you are logged in or not. Unfortunately there are a few problems with cookies that needed addressing. Let's toughen up our cookies!


cookie monster


Existing protections

We already have a couple of options for protecting our cookies so we should start with those. There are 2 flags that we can set on a cookie, HttpOnly and Secure.


HttpOnly

The HttpOnly flag is an optional flag that can be included in a Set-Cookie header to tell the browser to prevent client side script from accessing the cookie. It's as simple as appending the value:

Set-Cookie: sess=123; path=/; HttpOnly

The biggest benefit here is protection against Cross-Site Scripting, or XSS. If a site has an XSS vulnerability then an attacker could exploit this to steal the cookies of a visitor, essentially taking over their session and logging in to the victim's account. All modern browsers support the HttpOnly flag and if a piece of JavaScript attempts to read a cookie with the HttpOnly flag set, the browser will return an empty string instead of the cookie itself. Unless you have a specific requirement to access the cookie with client-side script, you should enable this flag.


Secure

The Secure flag is another optional flag that can be included in a Set-Cookie header that instructs the browser that the cookie must only ever be sent over a secure connection. You can see it on the end of this header:

Set-Cookie: sess=123; path=/; Secure

Because a session cookie is incredibly sensitive it shouldn't be sent over an insecure connection as it would be trivial for an attacker to intercept it and abuse it. When the Secure flag is set the browser will not send the cookie over an insecure connection. The Secure flag is also supported by all modern browsers and if you serve your site over HTTPS then you should set this flag on your cookies.


Same-Site Cookies

The Same-Site Cookies specification is still a draft but this new flag offers some very nice protection for our cookies. The main things to gain here are protection against Cross-Site Request Forgery (CSRF) attacks and Cross-Site Script Inclusion (XSSI). CSRF relies on an attacker being able to send requests to another site from your browser. If you load a hostile page that submits requests to Facebook, your browser will attach any cookies it has for Facebook to the requests sent. The attacker doesn't get to see the content of your cookie, or the response from Facebook, but they may not need to, simply executing the request with your authority is bad enough. XSSI is similar in that it relies on the browser attaching your cookie, and thus authority and authenticated state, to requests to load resources from <script> and <link> tags. The response to these requests could be different based on your cookie. Both of these problems, and others, can be mitigated with Same-Site Cookies. You can check section 1.1 of the spec for more details. The Same-Site flag has two possible values:


Lax

The Lax value for Same-Site is a good first step and offers a reasonable level of protection. You set it like any other flag:

Set-Cookie: sess=123; path=/; SameSite=Lax

With this flag set the browser will provide some protection against things like CSRF and XSSI, but not full protection, it should only be used as a defense in depth measure. For cross-site requests that use an unsafe HTTP method, like POST, the browser will not attach the cookie. That is, if Facebook set the SameSite=Lax flag on their cookies, and this page issued a POST request to facebook.com, then the browser would not attach your cookie for facebook.com to the request.


Strict

The Strict value offers much more protection but does have some drawbacks. You set the flag on the cookie just like before:

Set-Cookie: sess=123; path=/; SameSite=Strict

According to the specification you can issue the SameSite flag without a value and Strict will be assumed:

Set-Cookie: sess=123; path=/; SameSite

With SameSite set to Strict the browser will now enforce a much higher level of protection for the cookie. The cookie will not be attached to any cross-site requests, even for what we might consider to be a safe HTTP method like GET for a top-level navigation. If I had a link on this page to facebook.com and you clicked that link, the browser would issue a GET request to facebook.com to take you there. In Strict mode the browser would not attach any cookies you had for facebook.com to the request, so you would not be logged in when you arrived, even if you were already logged in. This is a really high level of protection and would stop any requests with GET parameters being able to do anything hostile, but could also be considered a little inconvenient.


How to use this flag will depend on the purpose of a particular cookie. You need to understand the behaviour of each flag and determine if it is appropriate. There is an example in section 5.2 of the spec and I'd recommend having a read through the rest of the document too. Starting out with Lax where you think it is appropriate and later moving up to Strict would be a good approach to test things out too.


Cookie Prefixes

Another great addition to help create tough cookies is the cookie prefix. This approach was devised to "smuggle cookie state to the server within the confines of the existing Cookie request header syntax". In other words, by implementing a new security feature without requiring changes/patches to servers/applications we are going to see much higher adoption a lot more quickly. There are 2 different prefixes that you can add to your cookie.


__Secure-

This prefix is the more relaxed in terms of the restrictions it applies but is still useful. You simply prefix your cookie and a compliant browser will enforce it:

Set-Cookie: __Secure-sess=123; path=/; Secure

There are 2 requirements for cookies that have the __Secure- prefix:

  1. Set with a "Secure" attribute
  2. Set from a URI whose "scheme" is considered "secure" by the user agent.

This means that if were to set this cookie without the Secure flag, the browser would not accept it, it would be rejected due to the lack of the Secure flag:

Set-Cookie: __Secure-sess=123; path=/

Likewise, the following cookie would also be rejected if it was set by http://scotthelme.co.uk because the scheme of the URI is not secure:

Set-Cookie: __Secure-sess=123; path=/; Secure

__Host-

The __Host- prefix is much more restrictive and offers a few additional protections. Again, you simply prefix the name of the cookie like so:

Set-Cookie: __Host-sess=123; path=/; Secure

There are 4 requirements the browser has to meet for cookies with the __Host- prefix:

  1. Set with a "Secure" attribute.
  2. Set from a URI whose "scheme" is considered "secure" by the user agent.
  3. Sent only to the host which set the cookie. That is, a cookie named "__Host-cookie1" set from "https://example.com" MUST NOT contain a "Domain" attribute (and will therefore be sent only to "example.com", and not to "subdomain.example.com").
  4. Sent to every request for a host. That is, a cookie named "__Host-cookie1" MUST contain a "Path" attribute with a value of "/".

As you can see we've taken the requirements from the __Secure- prefix and built on them adding 2 more. This would result in all of the following cookies being rejected even if they were set on a secure scheme:

Set-Cookie: __Host-sess=123
Set-Cookie: __Host-sess=123; Secure
Set-Cookie: __Host-sess=123; Domain=example.com
Set-Cookie: __Host-sess=123; Domain=example.com; Path=/
Set-Cookie: __Host-sess=123; Domain=example.com; Path=/; Secure

The Perfect Cookie?

From the above can we arrive at a Perfect Cookie(TM)? No, simply because nothing is ever perfect! What we can do is arrive at what is presently the best possible cookie we can bake:

Set-Cookie: __Host-sess=123; path=/; Secure; HttpOnly; SameSite=Lax

We're using the __Host- prefix which means the Secure flag has to be set and it has to be served from a secure host, there is no Domain attribute set and the path is /, I've set HttpOnly for XSS protection and finally SameSite is enabled in lax mode becuse you need to check if 'strict' is suitable for you. If you can meet the requirements for any of these then you should enable them on your cookies. Happy baking! 🍪