SameSite cookies explained
Learn how to mark your cookies for first-party and third-party usage with the SameSite attribute. You can enhance your site's security by using SameSite's Lax and Strict values to improve protection against CSRF attacks. Specifying the new None attribute allows you to explicitly mark your cookies for cross-site usage.
Secure your site by learning how to explicitly mark your cross-site cookies.
Cookies are one of the methods available for adding persistent state to web sites. Over the years their capabilities have grown and evolved, but left the platform with some problematic legacy issues. To address this, browsers (including Chrome, Firefox, and Edge) are changing their behavior to enforce more privacy-preserving defaults.
Each cookie is a key=value
pair along with a number of attributes that control when and where that cookie is used. You've probably already used these attributes to set things like expiration dates or indicating the cookie should only be sent over HTTPS. Servers set cookies by sending the aptly-named Set-Cookie
header in their response. For all the detail you can dive into RFC6265bis, but for now here's a quick refresher.
Say you have a blog where you want to display a "What's new" promo to your users. Users can dismiss the promo and then they won't see it again for a while. You can store that preference in a cookie, set it to expire in a month (2,600,000 seconds), and only send it over HTTPS. That header would look like this:
Set-Cookie: promo_shown=1; Max-Age=2600000; Secure
Servers set cookies using the Set-Cookie
header.
When your reader views a page that meets those requirements, i.e. they're on a secure connection and the cookie is less than a month old, then their browser will send this header in its request:
Cookie: promo_shown=1
Your browser sends cookies back in the Cookie
header.
You can also add and read the cookies available to that site in JavaScript using document.cookie
. Making an assignment to document.cookie
will create or override a cookie with that key. For example, you can try the following in your browser's JavaScript console:
> document.cookie = "promo_shown=1; Max-Age=2600000; Secure"< "promo_shown=1; Max-Age=2600000; Secure"
Reading document.cookie
will output all the cookies accessible in the current context, with each cookie separated by a semicolon:
> document.cookie;< "promo_shown=1; color_theme=peachpuff; sidebar_loc=left"
JavaScript can access cookies using document.cookie
.
If you try this on a selection of popular sites you will notice that most of them set significantly more than just three cookies. In most cases, those cookies are sent on every single request to that domain, which has a number of implications. Upload bandwidth is often more restricted than download for your users, so that overhead on all outbound requests is adding a delay on your time to first byte. Be conservative in the number and size of cookies you set. Make use of the Max-Age
attribute to help ensure that cookies don't hang around longer than needed.
What are first-party and third-party cookies? #
If you go back to that same selection of sites you were looking at before, you probably noticed that there were cookies present for a variety of domains, not just the one you were currently visiting. Cookies that match the domain of the current site, i.e. what's displayed in the browser's address bar, are referred to as first-party cookies. Similarly, cookies from domains other than the current site are referred to as third-party cookies. This isn't an absolute label but is relative to the user's context; the same cookie can be either first-party or third-party depending on which site the user is on at the time.
Cookies may come from a variety of different domains on one page.
Continuing the example from above, let's say one of your blog posts has a picture of a particularly amazing cat in it and it's hosted at /blog/img/amazing-cat.png
. Because it's such an amazing image, another person uses it directly on their site. If a visitor has been to your blog and has the promo_shown
cookie, then when they view amazing-cat.png
on the other person's site that cookie will be sent in that request for the image. This isn't particularly useful for anyone since promo_shown
isn't used for anything on this other person's site, it's just adding overhead to the request.
If that's an unintended effect, why would you want to do this? It's this mechanism that allows sites to maintain state when they are being used in a third-party context. For example, if you embed a YouTube video on your site then visitors will see a "Watch later" option in the player. If your visitor is already signed in to YouTube, that session is being made available in the embedded player by a third-party cookie—meaning that "Watch later" button will just save the video in one go rather than prompting them to sign in or having to navigate them away from your page and back over to YouTube.
A cookie in a third-party context is sent when visiting different pages.
One of the cultural properties of the web is that it's tended to be open by default. This is part of what has made it possible for so many people to create their own content and apps there. However, this has also brought a number of security and privacy concerns. Cross-site request forgery (CSRF) attacks rely on the fact that cookies are attached to any request to a given origin, no matter who initiates the request. For example, if you visit evil.example
then it can trigger requests to your-blog.example
, and your browser will happily attach the associated cookies. If your blog isn't careful with how it validates those requests then evil.example
could trigger actions like deleting posts or adding their own content.
Users are also becoming more aware of how cookies can be used to track their activity across multiple sites. However until now there hasn't been a way to explicitly state your intent with the cookie. Your promo_shown
cookie should only be sent in a first-party context, whereas a session cookie for a widget meant to be embedded on other sites is intentionally there for providing the signed-in state in a third-party context.
Explicitly state cookie usage with the SameSite
attribute #
The introduction of the SameSite
attribute (defined in RFC6265bis) allows you to declare if your cookie should be restricted to a first-party or same-site context. It's helpful to understand exactly what 'site' means here. The site is the combination of the domain suffix and the part of the domain just before it. For example, the www.web.dev
domain is part of the web.dev
site.
Key Term:
If the user is on www.web.dev
and requests an image from static.web.dev
then that is a same-site request.
The public suffix list defines this, so it's not just top-level domains like .com
but also includes services like github.io
. That enables your-project.github.io
and my-project.github.io
to count as separate sites.
Key Term:
If the user is on your-project.github.io
and requests an image from my-project.github.io
that's a cross-site request.
Introducing the SameSite
attribute on a cookie provides three different ways to control this behaviour. You can choose to not specify the attribute, or you can use Strict
or Lax
to limit the cookie to same-site requests.
If you set SameSite
to Strict
, your cookie will only be sent in a first-party context. In user terms, the cookie will only be sent if the site for the cookie matches the site currently shown in the browser's URL bar. So, if the promo_shown
cookie is set as follows:
Set-Cookie: promo_shown=1; SameSite=Strict
When the user is on your site, then the cookie will be sent with the request as expected. However when following a link into your site, say from another site or via an email from a friend, on that initial request the cookie will not be sent. This is good when you have cookies relating to functionality that will always be behind an initial navigation, such as changing a password or making a purchase, but is too restrictive for promo_shown
. If your reader follows the link into the site, they want the cookie sent so their preference can be applied.
That's where SameSite=Lax
comes in by allowing the cookie to be sent with these top-level navigations. Let's revisit the cat article example from above where another site is referencing your content. They make use of your photo of the cat directly and provide a link through to your original article.
<p>Look at this amazing cat!</p><img src="https://blog.example/blog/img/amazing-cat.png" /><p>Read the <a href="https://blog.example/blog/cat.html">article</a>.</p>
And the cookie has been set as so:
Set-Cookie: promo_shown=1; SameSite=Lax
When the reader is on the other person's blog the cookie will not be sent when the browser requests amazing-cat.png
. However when the reader follows the link through to cat.html
on your blog, that request will include the cookie. This makes Lax
a good choice for cookies affecting the display of the site with Strict
being useful for cookies related to actions your user is taking.
Caution:
Neither Strict
nor Lax
are a complete solution for your site's security. Cookies are sent as part of the user's request and you should treat them the same as any other user input. That means sanitizing and validating the input. Never use a cookie to store data you consider a server-side secret.
Finally there is the option of not specifying the value which has previously been the way of implicitly stating that you want the cookie to be sent in all contexts. In the latest draft of RFC6265bis this is being made explicit by introducing a new value of SameSite=None
. This means you can use None
to clearly communicate that you intentionally want the cookie sent in a third-party context.
Explicitly mark the context of a cookie as None
, Lax
, or Strict
.
If you provide a service that other sites consume such as widgets, embedded content, affiliate programs, advertising, or sign-in across multiple sites then you should use None
to ensure your intent is clear.
Changes to the default behavior without SameSite #
While the SameSite
attribute is widely supported, it has unfortunately not been widely adopted by developers. The open default of sending cookies everywhere means all use cases work but leaves the user vulnerable to CSRF and unintentional information leakage. To encourage developers to state their intent and provide users with a safer experience, the IETF proposal, Incrementally Better Cookies lays out two key changes:
- Cookies without a
SameSite
attribute will be treated asSameSite=Lax
. - Cookies with
SameSite=None
must also specifySecure
, meaning they require a secure context.
Chrome implements this default behavior as of version 84. Firefox has them available to test as of Firefox 69 and will make them default behaviors in the future. To test these behaviors in Firefox, open about:config
and set network.cookie.sameSite.laxByDefault
. Edge also plans to change its default behaviors.
This article will be updated as additional browsers announce support.
SameSite=Lax
by default #
No attribute set
Set-Cookie: promo_shown=1
If you send a cookie without any SameSite
attribute specified…
Default behavior applied
Set-Cookie: promo_shown=1; SameSite=Lax
The browser will treat that cookie as if SameSite=Lax
was specified.
While this is intended to apply a more secure default, you should ideally set an explicit SameSite
attribute rather than relying on the browser to apply that for you. This makes your intent for the cookie explicit and improves the chances of a consistent experience across browsers.
Caution:
The default behaviour applied by Chrome is slightly more permissive than an explicit SameSite=Lax
as it will allow certain cookies to be sent on top-level POST requests. You can see the exact details on the blink-dev announcement. This is intended as a temporary mitigation, you should still be fixing your cross-site cookies to use SameSite=None; Secure
.
SameSite=None
must be secure #
Rejected
Set-Cookie: widget_session=abc123; SameSite=None
Setting a cookie without Secure
will be rejected.
Accepted
Set-Cookie: widget_session=abc123; SameSite=None; Secure
You must ensure that you pair SameSite=None
with the Secure
attribute.
You can test this behavior as of Chrome 76 by enabling about://flags/#cookies-without-same-site-must-be-secure
and from Firefox 69 in about:config
by setting network.cookie.sameSite.noneRequiresSecure
.
You will want to apply this when setting new cookies and actively refresh existing cookies even if they are not approaching their expiry date.
If you rely on any services that provide third-party content on your site, you should also check with the provider that they are updating their services. You may need to update your dependencies or snippets to ensure that your site picks up the new behavior.
Both of these changes are backwards-compatible with browsers that have correctly implemented the previous version of the SameSite
attribute, or just do not support it at all. By applying these changes to your cookies, you are making their intended use explicit rather than relying on the default behavior of the browser. Likewise, any clients that do not recognize SameSite=None
as of yet should ignore it and carry on as if the attribute was not set.
Warning:
A number of older versions of browsers including Chrome, Safari, and UC browser are incompatible with the new None
attribute and may ignore or restrict the cookie. This behavior is fixed in current versions, but you should check your traffic to determine what proportion of your users are affected. You can see the list of known incompatible clients on the Chromium site.
SameSite
cookie recipes #
For further detail on exactly how to update your cookies to successfully handle these changes to SameSite=None
and the difference in browser behavior, head to the follow up article, SameSite cookie recipes.
Kind thanks for contributions and feedback from Lily Chen, Malte Ubl, Mike West, Rob Dodson, Tom Steiner, and Vivek Sekhar
Cookie hero image by Pille-Riin Priske on Unsplash
Last updated: May 28, 2020 Improve article
Thank you for the feedback!
Comments
Post a Comment
Share your thoughts!