https://simonwillison.net/2021/Aug/3/samesite/
Simon Willison's Weblog
Exploring the SameSite cookie attribute for preventing CSRF
In reading Yan Zhu's excellent write-up of the JSON CSRF
vulnerability she found in OkCupid one thing puzzled me: I was under
the impression that browsers these days default to treating cookies
as SameSite=Lax, so I would expect attacks like the one Yan described
not to work in modern browsers.
This lead me down a rabbit hole of exploring how SameSite actually
works, including building an interactive SameSite cookie exploration
tool along the way. Here's what I learned.
Background: Cross-Site Request Forgery
I've been tracking CSRF (Cross-Site Request Forgery) on this blog
since 2005(!)
A quick review: let's say you have a page in your application that
allows a user to delete their account, at https://www.example.com/
delete-my-account. The user has to be signed in with a cookie in
order to activate that feature.
If you created that page to respond to GET requests, I as an evil
person could create a page at https://www.evil.com/
force-you-to-delete-your-account that does this:
If I can get you to visit my page, I can force you to delete your
account!
But you're smarter than that, and you know that GET requests should
be idempotent. You implement your endpoint to require a POST request
instead.
Turns out I can still force-delete accounts, if I can trick a user
into visiting a page with the following evil HTML on it:
The form submits with JavaScript the instant they load the page!
CSRF is an extremely common and nasty vulnerability--especially since
it's a hole by default: if you don't know what CSRF is, you likely
have it in your application.
Traditionally the solution has been to use CSRF tokens--hidden form
fields which "prove" that the user came from a form on your own site,
and not a form hosted somewhere else. OWASP call this the Double
Submit Cookie pattern.
Web frameworks like Django implement CSRF protection for you. I built
asgi-csrf to help add CSRF token protection to ASGI applications.
Enter the SameSite cookie attribute
Clearly it would be better if we didn't have to worry about CSRF at
all.
As far as I can tell, work on specifying the SameSite cookie
attribute started in June 2016. The idea was to add an additional
attribute to cookies that specifies the policy for if they should be
included in requests made to a domain from pages hosted on another
domain.
Today, all modern browsers support SameSite. MDN has SameSite
documentation, but a summary is:
* SameSite=None--the cookie is sent in "all contexts"--more-or-less
how things used to work before SameSite was invented.
* SameSite=Strict--the cookie is only sent for requests that
originate on the same domain. Even arriving on the site from an
off-site link will not see the cookie, unless you subsequently
refresh the page or navigate within the site.
* SameSite=Lax--cookie is sent if you navigate to the site through
following a link from another domain but not if you submit a
form. This is generally what you want to protect against CSRF
attacks!
The attribute is specified by the server in a set-cookie header that
looks like this:
set-cookie: lax-demo=3473; Path=/; SameSite=lax
Why not habitually use SameSite=Strict? Because then if someone
follows a link to your site their first request will be treated as if
they are not signed in at all. That's bad!
So explicitly setting a cookie with SameSite=Lax should be enough to
protect your application from CSRF vulnerabilities... provided your
users have a browser that supports it.
(Can I Use reports 93.95% global support for the attribute--not quite
high enough for me to stop habitually using CSRF tokens, but we're
getting there.)
What if the SameSite attribute is missing?
Here's where things get interesting. If a cookie is set without a
SameSite attribute at all, how should the browser treat it?
Over the past year, all of the major browsers have been changing
their default behaviour. The goal is for a missing SameSite attribute
to be treated as if it was SameSite=Lax--providing CSRF protection by
default.
I have found it infuriatingly difficult to track down if and when
this change has been made:
* Chrome/Chromium offer the best documentation--they claim to have
ramped up the new default to 100% of users in August 2020.
WebViews in Android still have the old default behaviour, which
is scheduled to be fixed in Android 12 (not yet released).
* Firefox have a blog entry from August 2020 which says "Starting
with Firefox 79 (June 2020), we rolled it out to 50% of the
Firefox Beta user base"--but I've not been able to find any
subsequent updates.
* I have no idea at all what's going on with Safari!
I started a Twitter thread to try and collect more information, so
please reply there if you know what's going on in more detail.
The Chrome 2-minute twist
Assuming all of the above, the mystery remained: how did Yan's
exploit fail to be prevented by browsers?
After some back-and-forth about this on Twitter Yan proposed that the
answer may be this detail, tucked away on the Chrome Platform Status
page for Feature: Cookies default to SameSite=Lax.
Note: Chrome will make an exception for cookies set without a
SameSite attribute less than 2 minutes ago. Such cookies will
also be sent with non-idempotent (e.g. POST) top-level cross-site
requests despite normal SameSite=Lax cookies requiring top-level
cross-site requests to have a safe (e.g. GET) HTTP method.
Support for this intervention ("Lax + POST") will be removed in
the future.
It looks like OkCupid were setting their authentication cookie
without a SameSite attribute... which opened them up to a form-based
CSRF attack but only for the 120 seconds following the cookie being
set!
Building a tool to explore SameSite browser behaviour
I was finding this all very confusing, so I built a tool.
A screenshot showing the two pages from the demo side-by-side
The code lives in simonw/samesite-lax-demo on GitHub, but the tool
itself has two sides:
* A server-side Python (Starlette) web application for setting
cookies with different SameSite attributes. This is hosted on
Vercel at https://samesite-lax-demo.vercel.app/
* An HTML page on a different domain that links to that cookied
site, provides a POST form targetting it, embeds an image from it
and executes some fetch() requests against it. This is at https:/
/simonw.github.io/samesite-lax-demo/
Hosting on two separate domains is critical for the tool to show what
is going on. I chose Vercel and GitHub Pages because they are both
trivial to set up to continuously deploy changes from a GitHub
repository.
Using the tool in different browsers helps show exactly what is going
on with regards to cross-domain cookies.
A few of the things I observed using the tool:
* SameSite=Strict works as you would expect. It's particularly
interesting to follow the regular link from the
static site to the application and see how the strict cookie is
NOT visible upon arrival--but becomes visible when you refresh
that page.
* I included a dynamically generated SVG in a image tag, which shows the cookies (using SVG
) that are visible to the request. That image shows all
four types of cookie when embedded on the Vercel domain, but when
embedded on the GitHub pages domain it differs wildly:
+ Firefox 89 shows both the SameSite=None and the missing
SameSite cookies
+ Chrome 92 shows just the SameSite=None cookie
+ Safari 14.0 shows no cookies at all!
* Chrome won't let you set a SameSite=None cookie without including
the Secure attribute.
* I also added some JavaScript that makes a cross-domain fetch(...,
{credentials: "include"}) call against a /cookies.json endpoint.
This didn't send any cookies at all until I added server-side
headers access-control-allow-origin: https://simonw.github.io and
access-control-allow-credentials: true. Having done that, I got
the same results across the three browsers as for the
I trick you into visiting my evil pge and you're now signed in to
that site using an account that I control. I cross my fingers and
hope you don't notice the "you are signed in as X" message in the UI.
An interesting thing about Login CSRF is that, since it involves
setting a cookie but not sending a cookie, SameSite=Lax would seem to
make no difference at all. You need to look to other mechanisms to
protect against this attack.
But actually, you can use SameSite=Lax to prevent these. The trick is
to only allow logins from users that are carrying at least one cookie
which you have set in that way--since you know that those cookies
could not have been sent if the user originated in a form on another
site.
Another option: check the HTTP Origin header on the oncoming request.
Final recommendations
As an application developer, you should set all cookies with SameSite
=Lax unless you have a very good reason not to. Most web frameworks
do this by default now--Django shipped support for this in Django 2.1
in August 2018.
Do you still need CSRF tokens as well? I think so: I don't like the
idea of users who fire up an older browser (maybe borrowing an
obsolete computer) being vulnerable to this attack, and I worry about
the subdomain issue described above.
And if you work for a browser vendor, please make it easier to find
information on what the default behaviour is and when it was shipped!
Posted 3rd August 2021 at 9:09 pm * Follow @simonw on Twitter
This is Exploring the SameSite cookie attribute for preventing CSRF
by Simon Willison, posted on 3rd August 2021.
cookies 25 csrf 47 security 377
Next: Apply conversion functions to data in SQLite columns with the
sqlite-utils CLI tool
Previous: Weeknotes: datasette-remote-metadata, sqlite-transform
--multi
Exploring the SameSite cookie attribute for preventing CSRF - my
notes on how SameSite actually works, based on explorations
conducted using a debugging tool I built https://t.co/7iIsDHpCYG
-- Simon Willison (@simonw) August 3, 2021
* Search
* (c)
* 2002
* 2003
* 2004
* 2005
* 2006
* 2007
* 2008
* 2009
* 2010
* 2011
* 2012
* 2013
* 2014
* 2015
* 2016
* 2017
* 2018
* 2019
* 2020
* 2021