The first article gave a brief explanation about what SameSite Cookies actually mean, but it was very brief indeed and quite a bit simplified. The other article focused on solving the Chrome vs. Safari implementations issue, and I wanted to keep the introduction short. However I feel this topic is very important and deserves an explanation that really answers all questions. So, without further ado, I want to take you on a journey into the deep rabbit hole of SameSite Cookies.
Cookies!
Back in the days of the good old web 1.0 cookies were implemented to preserve and persist state information on the side of the User Agent (mostly a web browser). The nitty gritty details are all written down in RFC 6265, but it all boils down to cookies being key-value pairs that can be written to the User Agent using the Set-Cookie
http header in the response. Besides the cookie name (the key) and its value, other options can additionally be set to control when a cookie should expire (expires
or the newer max-age
), if it may be sent via http or only via https (Secure
), and if JavaScript in the browser should be able to read and write the cookie value or if the cookie should be hidden from JavaScript and only be sent to the server (HttpOnly
).
If a cookie is set and not yet expired, the User Agent sends the cookie along to the server with every request that matches the domain
and path
values that can also be set on a cookie. What do they do?
Let’s assume there is a website on www.example.com
and it allows you to log in to customize your experience. Your log in session is stored in a session cookie. The website also offers a store at shop.example.com
which uses the same login mechanism. With the default behavior, the cookie would be set to www.example.com
and not be sent to shop.example.com
, so you would have to log in at both subdomains explicitly. When the website sets the cookie using the domain=example.com
parameter, the cookie will be sent to all subdomains of example.com, and so the user only needs to login once. You can only use the domain
option to narrow or broaden the scope of the cookie on your own domain: The value set of the domain
option must be part of the host name that is sending the Set-Cookie
header. So you can’t set a cookie for thinktecture.com
as that would introduce a security issue.
The path
allows you to further narrow down the places where a cookie will be sent to. If, for example, the website sets the cookie and states domain=example.com; path=/blog
, then the cookie will only be sent to the server if the request starts with /blog
, so it would be sent to www.example.com/blog
and shop.example.com/blogroll
, but not to example.com/blo
.
Security crumbles
Without cookies the web would not work as it does today. However, since the mid 90ies when cookies were invented, the web changed quite a lot and so did the attack methods on web applications. Let’s think through a possible attack that could happen:
Imagine that after login our web server at www.example.com
sets a session cookie like this: Set-Cookie: session=89bea4bb-85d1-4f8b-b4fa-a9b8db015e2b; Max-Age=2600000; Domain=example.com; Path=/; Secure; HttpOnly
. Whenever the browser sends a request using https, our server can nosh the fine cookie and from its taste it knows that there’s a session with the GUID 89bea4bb-85d1-4f8b-b4fa-a9b8db015e2b
.It can then answer the request and amend it with information from the user that is currently logged in with this session id. The web shop at shop.example.com
is a single-page web application and it communicates with api.example.com
, which also uses the same session cookie information.
An attacker now sets up a new websites. Let’s call it example-com-product-reviews.attacker.com
. The attacker lures users onto his website by providing additional information for products only found on shop.example.com
. When a user visits his site, he returns a website with some content and additional JavaScript. The JavaScript can’t read our session cookie. First of all, example-com-product-reviews.attacker.com
is not matched by the example.com
domain option, and secondly the cookie is set to HttpOnly
, so JavaScript can’t touch it anyways. This way the attacking JavaScript can’t do anything with our cookie directly. But what the JavaScript can do is use the Fetch API
(the modern replacement of the old XMLHttpRequest
object) to send a request to the shops API. It could, for example send a malicious request to POST https://api.example.com/addItemToCart?itemId=123
.
While the user reads the information presented on example-com-product-reviews.attacker.com
, the JavaScript executes and the browser sends the request. Since this request is sent to api.example.com
, which matches the domain option from our session cookie, and the path also matches, the browser indeed attaches the cookie to the request. The API server receives the request, finds the attached session cookie, determines the user and adds a very expensive item to the shopping cart – without the user even noticing. Another forged request like POST https://api.example.com/checkoutUsingAlreadyProvidedPaymentMethod
could then cause real damage.
This attack method is known as Cross Site Request Forgery, or short CSRF. If you want you can read more about that in the CRSF Page on OWASP.
Now, there is another technology in place that could help prevent these kinds of attacks. It is called CORS (Cross Origin Resource Sharing) and involves a bit of knowledge on how to apply it correctly. For now, we assume that somehow the common development setting Allow any origin
made it on the production server on api.example.com
instead of a specific setting to only allow requests from shop.example.com
. And while this might sound a bit constructed, configuration mistakes can happen – and sadly they also do happen all the time. It just takes a project setup at a time where the final domains aren’t known yet, so AnyOrigin
is used for development purposes. Later when the shop needs to be rolled out everybody is so stressed out that nobody thinks about checking and reviewing all these // Todo: Configure this to the actual shop domain, OR ELSE!
comments left in the codebase – in good faith that this will be done before release.
But its not only POST requests: Since you are logged in, example-com-product-reviews.attacker.com
can also add an image tag to its website which looks like this: <img src="https://shop.example.com/profilepicture.jpg" />
. The browser will send a GET request to the shop requesting the current users profile image, and it will send the cookie with that request. The server is able to respond with the correct image. This way the attacker can display the correct profile image of the current user on their website, which may make them appear even more legitimate.
But not everything is bad: where we need cross-site cookies
So, our main issue here is that a browser will happily send along all cookies of a domain with every request to that domain. In the previous scenario this is an attack, but there might be occasions where someone actually wants and requires this scenario.
Imagine you build a website and embed a YouTube video. The video is requested from Googles YouTube servers. If you are logged in to YouTube, your login cookie gets sent along just as with the profile picture in the example above. YouTube now is able to offer the user a ‘Watch later’ button or the possibility to add the video to the users personal playlist.
This is a desired feature, so we have to think about how to block malicious attacks while still being open for legitimate usage.
All cookies are equal, but some are more equal than others
Before we delve into the actual SameSite Cookie option that can help us with that, I want to take a minute to differentiate these cookies a bit more.
First of all, we have first-party cookies. These are the cookies that belong to the current domain as by their settings. If you are on example-com-product-reviews.attacker.com
, all cookies with the domain set to exactly this or to attacker.com
are considered first party cookies. Then there are third-party cookies. This are all cookies for all other sites, like example.com
or youtube.com
.
As you see, a cookie can be both a first-party cookie and a third-party cookie, just not at the same time, depending on the domain the user currently is at.
But what is the ‘same’ domain as per the SameSite rules? Usually, the combination of second (example.com) and top level domain (example.com) is the common denominator. For example when the web application served from www.example.com
sends a request to shop.example.com
, this would be considered as a same-site request.
However, there are exceptions from that. One prominent example would be co.uk
or co.jp
, where commercial domain registrants get a third level domain all under the co
second level domain. Requests between mysite.co.uk
and attacker.co.uk
should not be same-site, although they are both under co.uk
. Another example are subdomains that can be used by different parties, for example mysite.azurewebsites.net
and attacker.azurewebsites.net
for Microsoft Azure or *.compute.amazonaws.com
for Amazons cloud services. Here you also want stronger isolation, as the sibling subdomains are absolutely not under your control.
Since the browser needs to decide whether a request is same-site or not, and technically anybody can register a domain and offer its subdomains for such purposes, there needs to be a way for browser vendors to figure out if a domain should be treated as same-site or not. This is where the Public Suffix List comes in handy. This list provides the domains that are a “public suffix” (aka under which different parties can register their own subdomains / prefixes).
Based on the information provided by this list, the Browsers are able to distinguish between first and third party cookies based on the current domain.
Controlling SameSite Cookie behavior
Now that we know that we have two types of cookies, we can start controlling what a browser does with them respectively.
The original SameSite policy was suggested in the Same-site Cookies draft. This draft specifies the new SameSite
option that is possible when setting a cookie and allows two values: Strict
and Lax
. This was designed as backwards-compatible by maintaining the original behavior when no SameSite
option is set at all. Sadly, this feature was not really broadly adopted.
Then M. West from Google published a new draft to the web standards track named Incrementally Better Cookies that introduced the new setting None
, requires the Secure
flag for SameSite=None
cookies and – this is the real game changer – changing the default behavior of cookies set with no SameSite option to Lax
(and thus breaking backwards-compatibility). The whole changes that are supposed to replace the old RFC 6265 are combined in the new Cookies: HTTP State Management Mechanism document.
Now, what do these SameSite Cookie options do?
Let’s go with the easiest settings first, None
: This is the same as the original behavior of cookies where the SameSite option did not exist: Every cookie will be sent along with every request, as long as the values of domain and path match.
The second is Strict
: This defines that cookies are only sent to the server when the request is initiated by a first-party context. What that actually means is that when you enter https://shop.example.com
into your Browsers address bar, or you click on a link to that URL, this very first request to the server is not yet a first-party context (as it was initiated by an unknown party). Only subsequent requests that originate from that loaded page are considered being in the first-party context and as such provide the cookie to the server.
Now, for our session cookie this is a bit too harsh: The first request to shop.example.com
would not provide the session cookie, so the initial response of the shop would be the same as for a not-logged in user, but the next navigation on the application would show the user as logged in.
This is where Lax
provides its services: This specifies that the cookie will be sent to the server also on top-level navigations to our site, i.e. when the user follows a link to our shop. So even on the first navigation to our site the cookie will be sent and the user immediately appears as logged in.
The most important thing is that Strict
and Lax
both will prevent sending of the cookies for all other requests from a 3rd party context. This means that in our example the web site served from example-com-product-reviews.attacker.com
both described attacks won’t work anymore. The image-tag requesting the user profile picture from our shop will not send the cookie along to shop.example.com
(so the fake page won’t be able to show the real users image). Nor will the browser send the cookie along with the POST requests to api.example.com
.
The Chrome vs. Safari issue
Most browser vendors already implemented the older Same-site Cookies suggestions. Chrome now rushes ahead implements the Incrementally Better Cookies specification starting with version 80 and changing the default to Lax
.
The problem with that is that Safari recognizes the SameSite option starting with version 12, but their implementation has a bug: It interprets invalid values as if SameSite=Strict
had been specified, and for it only Strict
and Lax
are valid values, as the older specification did not yet specify None
.
This bug is fixed in Safari 13, but sadly the Safari team stated that this won’t be backported and older iOS and macOS versions won’t be updated anymore with that fix. The actual problem is that a web server can’t satisfy the bugged implementations in Safari and the new Chrome implementation with a single approach. It is, sadly, required to detect if the browser is one of the affected Safari versions and omit the SameSite option when sending cookies to these browsers.
CanIUse.com provides a nice overview about the different browsers and versions and whether SameSite cookies are supported or not on their ‘SameSite’ cookie attribute page.
This is how the most common browsers interpret the SameSite setting. (Browser versions older than the lowest specified version do not yet implement SameSite at all and consider everything as not specified):
Browser | Not specified | None | Lax | Strict |
---|---|---|---|---|
Chrome >= 51 | None | Ignored | Lax | Strict |
Chrome >= 67 | None | None | Lax | Strict |
Chrome >= 80 | Lax | None | Lax | Strict |
Safari >= 12 | None | Strict | Lax | Strict |
Safari >= 13 | None | None | Lax | Strict |
Firefox >= 60 | None | None | Lax | Strict |
IE >= 11 | None | None | Lax | Strict |
Edge >= 16 | None | None | Lax | Strict |
Both Firefox and Edge have stated an ‘Intent to implement’ for the Incrementally Better Cookies draft. They, too, will change their default behavior to SameSite=Lax
and additionally require Secure
to be specified for Cookies with the SameSite=None
option, but as of now there is no information about when that will happen (Source: for Firefox, and for Edge).
Summary
We explored how cookies worked since the dawn of the world wide web, and how that could be abused by CSRF attacks. We then learned about first- and third party cookies and the Public Suffix List.
Then we learned what changes were proposed with the Same-Site options for cookies, and how the Chrome team at Google now pushes forward to get these security improvements out into the field.
Lastly we looked ad the different implementations of the SameSite option and how that might affect the way we have to set cookies for certain browser versions.
Hopefully, you learned a bit more about Cookies, the SameSite attribute and how to handle them with care.