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!
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 POST
ed. 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
orhttps
- Have the same domain i.e.
example.com
,andrewlock.net
ormicrosoft.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
orhttps
- Have the same domain i.e.
example.com
,andrewlock.net
ormicrosoft.com
- Have the same subdomain i.e.
www.
- Have the same port (which may be implicit) i.e. port
80
forhttp
and443
forhttps
If we use the URL http://www.example.org
and compare against variations, you can see the difference more clearly.
URL | Description | same-site | same-origin |
---|---|---|---|
http://www.example.org | Identical URL | ✅ | ✅ |
http://www.example.org:80 | Identical URL (implicit port) | ✅ | ✅ |
http://www.example.org:8080 | Different port | ✅ | ❌ |
http://sub.example.org | Different subdomain | ✅ | ❌ |
https://www.example.org | Different scheme | ❌ | ❌ |
http://www.example.evil | Different 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 cookievalue
—(required) The value of the cookieHttpOnly
—When set, the cookie can't be accessed from JavaScriptSecure
—When set, the cookie won't be sent forhttp:
requests, onlyhttps:
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 ifSameSite
is not explicitly set on the cookie.None
—This can only be used if the cookie is also markedSecure
; settingSameSite=None
without theSecure
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 theSameSite=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 includeStrict
cookies. - If your site is embedded in an
iframe
on a site hosted on a different domain, your site won't receive anyStrict
cookies. - An image on your website is linked to directly in the
src
attribute of an<img>
from another site
SameSite=Strict
cookie advantages
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.
SameSite=Strict
cookie disadvantages
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 setSameSite=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
(orStrict
) 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 includeLax
(orStrict
) cookies. - If your site is embedded in an
iframe
on a site hosted on a different domain, your site won't receive anyLax
(orStrict
) cookies. - An image on your website is linked to directly in the
src
attribute of an<img>
from another site.
SameSite=Lax
cookie advantages
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.
SameSite=Lax
cookie disadvantages
Aside from not being as secure as SameSite=Strict
, there are still some scenarios that don't work with SameSite=Lax
:
- 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 setSameSite=Lax
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=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 soSameSite=None; Secure
. IfSecure
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.
SameSite=None
cookie advantages
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 withresponse_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.
SameSite=None
cookie disadvantages
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 the2016
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 theSameSite
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.