blog post image
Andrew Lock avatar

Andrew Lock

~11 min read

Understanding SameSite cookies

In my previous post, I described a problem with sending cross-origin requests, and the problem was down to SameSite cookies. In this post I look at SameSite cookies in more detail. I'll describe what they are, why they're useful, and what problems they can cause.

Understanding Cross-Site Request Forgery attacks

SameSite cookies are designed as a line of defence against Cross-Site Request Forgery (CSRF) attacks. To understand why SameSite cookies are useful, we first need to understand CSRF attacks.

The following is an excerpt from my new book ASP.NET Core in Action, Third Edition. If you like what you see, consider picking up a copy! 🙂

CSRF attacks can be a problem for websites or APIs that use cookies for authentication. A CSRF attack involves a malicious website making an authenticated request to your API on behalf of the user, without the user’s initiating the request. In this section we’ll explore how these attacks work and how you can mitigate them with antiforgery tokens.

The canonical example of this attack is a bank transfer/withdrawal. Imagine you have a banking application that stores authentication tokens in a cookie, as is common (especially in traditional server-side rendered applications). Browsers automatically send the cookies associated with a domain with every request so the app knows whether a user is authenticated.

Now imagine your application has a page that lets a user transfer funds from their account to another account using a POST request to the Balance Razor Page. You have to be logged in to access the form (you’ve protected the Razor Page with the [Authorize] attribute or global authorization requirements), but otherwise you post a form that says how much you want to transfer and where you want to transfer it. Seems simple enough?

Suppose that a user visits your site, logs in, and performs a transaction. Then they visit a second website that the attacker has control of. The attacker has embedded a form in their website that performs a POST to your bank’s website, identical to the transfer-funds form on your banking website. This form does something malicious, such as transfer all the user’s funds to the attacker, as shown in the following figure. Browsers automatically send the cookies for the application when the page does a full form post, and the banking app has no way of knowing that this is a malicious request. The unsuspecting user has given all their money to the attacker!

A CSRF attack occurs when a logged-in user visits a malicious site. The malicious site crafts a form that matches one on your app and POSTs it to your app. The browser sends the authentication cookie automatically, so your app sees the request as a valid request from the user. From ASP.NET Core in Action, Third Edition

The vulnerability here revolves around the fact that browsers automatically send cookies when a page is requested (using a GET request) or a form is POSTed. There’s no difference between a legitimate POST of the form in your banking app and the attacker’s malicious POST. Unfortunately, this behavior is baked into the web; it’s what allows you to navigate websites seamlessly after initially logging in.

This is where SameSite cookies come in. But first we need to take a quick detour to look at the difference between "same-site" and "same-origin".

"same-site" vs "same-origin"

When we look at the details of SameSite cookies, we're going to be dealing a lot with the concept of "same-site" and "cross-site" requests. This is similar to another phrase you may have heard, "cross-origin" requests, but they are subtly different.

In summary, two URLs are considered to be "same-site" if they:

  • Have the same scheme i.e. http or https
  • Have the same domain i.e. example.com, andrewlock.net or microsoft.com

They don't need to have the same port or subdomain.

Two URLs are considered to be "same-origin" if they

  • Have the same scheme i.e. http or https
  • Have the same domain i.e. example.com, andrewlock.net or microsoft.com
  • Have the same subdomain i.e. www.
  • Have the same port (which may be implicit) i.e. port 80 for http and 443 for https

If we use the URL http://www.example.org and compare against variations, you can see the difference more clearly.

URLDescriptionsame-sitesame-origin
http://www.example.orgIdentical URL
http://www.example.org:80Identical URL (implicit port)
http://www.example.org:8080Different port
http://sub.example.orgDifferent subdomain
https://www.example.orgDifferent scheme
http://www.example.evilDifferent TLD

When thinking about SameSite cookies, we're only thinking about "same-site" or "cross-site".

What are SameSite cookies, and how do they protect against CSRF?

A cookie is an HTTP header that can be set in an HTTP response. The browser then sends that cookie with subsequent requests to the site. The cookie has two required attributes, and various optional values, but I'm just going to focus on a few here:

  • name(required) The key/identifier for the cookie
  • value(required) The value of the cookie
  • HttpOnly—When set, the cookie can't be accessed from JavaScript
  • Secure—When set, the cookie won't be sent for http: requests, only https:
  • SameSite—Controls whether or not a cookie is sent with cross-site requests

In practice a cookie header using these options looks something like this:

Set-Cookie: MyCookie=TheValue; Secure; HttpOnly; SameSite=Lax

So SameSite is an option you can apply to "normal" cookies. There are three different values you can use for SameSite:

  • Strict
  • Lax—In the current standard (more on that later), this is the default behaviour if SameSite is not explicitly set on the cookie.
  • None—This can only be used if the cookie is also marked Secure; setting SameSite=None without the Secure flag may lead to the cookie being rejected.

I'll walk through each of these settings, describe what they do, what actions they allow and disallow, and the pros and cons of each.

SameSite=Strict cookies

SameSite=Strict is the most restrictive option you can use. From the MDN documentation on Strict cookies:

Strict Means that the browser sends the cookie only for same-site requests, that is, requests originating from the same site that set the cookie. If a request originates from a different domain or scheme (even with the same domain), no cookies with the SameSite=Strict attribute are sent”

So to summarise, Strict cookies are only sent for same-site requests, not cross-site requests.

SameSite=Strict cookies are sent

  • You type the URL into the address bar and navigate directly to a site.
  • You refresh the page using the browser.
  • You follow an <a> link to a page from within the same site.
  • You embed an iframe that is hosted on the same site (same domain and scheme)
  • You make an AJAX/fetch same-site request using JavaScript

SameSite=Strict cookies are not sent

  • You follow an <a> link to your site from a different domain. The cookies will not be sent for the initial page load.
  • A form embedded on another site sends data to your website. Strict cookies won't be included in the request.
  • A site on another domain makes an AJAX/fetch request using JavaScript to your site won't include Strict cookies.
  • If your site is embedded in an iframe on a site hosted on a different domain, your site won't receive any Strict cookies.
  • An image on your website is linked to directly in the src attribute of an <img> from another site

Using SameSite=Strict provides a huge defence against CSRF attacks. The attacker in a CSRF attack relies on the victim being logged in to the website, so that the attacker can perform an action on behalf of the victim. This requires that the authentication cookies be automatically sent by the browser, but if the authentication cookies are set with Strict mode, that won't happen!

SameSite=Strict pretty much nullifies this approach to CSRF entirely, so it provides a big security boost.

As is common, increasing your security by SameSite=Strict can be inconvenient:

  • If users follow links to your website, by clicking a link in an email for example, then they won't appear logged in. On subsequent requests they will appear logged in, but your application needs to be able to handle that initial unauthenticated request gracefully.
  • Some authentication mechanisms (e.g. OpenID Connect with response_mode=form_post) rely on cross-site form-posts, so you wouldn't be able to set SameSite=Strict for the authentication cookie in this case.
  • If you need your site to be embedded as an iframe in a cross-site way you won't be able to send cookies.
  • If you need cookies to be sent for cross-site AJAX requests you can't use SameSite=Strict.

SameSite=Lax cookies

SameSite=Lax is the default mode used when you don't explicitly specify a SameSite mode (this changed in 2019 as I'll discuss later). From the MDN documentation:

Lax Means that the cookie is not sent on cross-site requests, such as on requests to load images or frames, but is sent when a user is navigating to the origin site from an external site (for example, when following a link).”

So Lax cookies are sent in all the same situations as Strict cookies, plus several additional scenarios.

SameSite=Lax cookies are sent

Lax cookies are sent for all the same scenarios as Strict:

  • You type the URL into the address bar and navigate directly to a site.
  • You refresh the page using the browser.
  • You follow a link to a page from within the same site.
  • You embed an iframe that is hosted on the same site (same domain and scheme)
  • You make an AJAX/fetch same-site request using JavaScript

In addition, Lax cookies are also sent for "top-level GET requests", that is, GET requests which change the URL in the navigation bar. That means they are additionally sent when:

  • You follow an <a> link to your site from a different domain.
  • A form embedded on another site sends data to your website using the GET action.

SameSite=Lax cookies are not sent

  • A form embedded on another site sends data to your website using POST. Lax (or Strict) cookies won't be included in the request.
  • A site on another domain makes an AJAX/fetch request using JavaScript to your site won't include Lax (or Strict) cookies.
  • If your site is embedded in an iframe on a site hosted on a different domain, your site won't receive any Lax (or Strict) cookies.
  • An image on your website is linked to directly in the src attribute of an <img> from another site.

Using SameSite=Lax provides a moderate defence against CSRF attacks, as cookies are not included for requests that are considered "unsafe" (e.g. POST requests). Theoretically, that means that even if you have a CSRF vulnerability, an attacker should not be able to exploit it, as they can only take "safe" actions. Of course, that relies on you definitely not doing anything unsafe in GET requests, so it doesn't provide as much safety as Strict.

However, the big advantage of Lax cookies is that you can follow links to your website and remain logged in. This is a big improvement in user experience, and removes the additional complexity required to provide a nice UX when using Strict cookies.

Aside from not being as secure as SameSite=Strict, there are still some scenarios that don't work with SameSite=Lax:

SameSite=None cookies

SameSite=None was introduced as a new SameSite mode in 2019, and it removes any same-site requirements from the cookie. However, you must mark the cookie as Secure. From the MDN documentation:

None Means that the browser sends the cookie with both cross-site and same-site requests. The Secure attribute must also be set when setting this value, like so SameSite=None; Secure. If Secure is missing an error will be logged”

So None cookies are always sent, regardless of whether you're in a same-site or cross-site scenario.

The one advantage of SameSite=None is that cookies are always sent, so if you need a cookie to be sent cross site, it's your only choice, Strict and Lax won't work. The scenarios where you will have to use None include:

  • A form embedded on another site sends data to your website using POST, for example as part of an OpenID Connect with response_mode=form_post flow
  • A site on another domain makes an AJAX/fetch request using JavaScript to your site.
  • Your site is embedded in an iframe on a site hosted on a different domain.
  • An image on your website is linked to directly in the src attribute of an <img> from another site.

The disadvantage of None cookies is that they do nothing to protect your from CSRF attacks, disabling the protections that Strict or Lax cookies would provide. For this reason, you generally shouldn't use SameSite=None by default. Only use it where it's strictly required.

Another problem is that None wasn't a valid option in the original 2016 draft of the standard. In the previous version of the standard, if you didn't set the SameSite mode, the browser would treat it the same as the None mode. In 2019 the standard changed so that these cookies would be treated as Lax instead.

The big problem is when you have cookies that you need to send cross-site. Setting None works for browsers that implement the 2019 version of the standard. However, browsers that implement the 2016 version typically treat "unknown" values as Strict, the opposite of what you want!

This essentially leaves you with two options:

  • Do user-agent sniffing and try to only set SameSite=None for browsers that implement the 2016 version of the standard. This is the approached described in the ASP.NET Core documentation.
  • Set two cookies with the same value, one with SameSite=None (for use by 2019 browsers), and one without setting the SameSite mode (for use by 2016 browsers). I'll show how to take this approach in my next post.

Hopefully, you won't have to deal with the old browser issue, but if your site has to handle both old and new browsers, then SameSite cookies can be problematic. In the next post I show and approach that aims to handle this gracefully.

Summary

In this post I described Cross-Site Request Forgery (CSRF) attacks. This vulnerability stems from the fact that browsers automatically send any cookies set for a domain. At least, that used to be the case. With SameSite cookies, the browser only sends cookies for "same-site" requests. For the remainder of the post I describe the three different modes for SameSite cookies—Strict, Lax, and None—, and the advantages and disadvantages of each.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?