The idea: your browser adds your cookie for you
Alice is logged in to her bank. The bank knows it is her because every request from her browser carries the session cookie it set at login. Then Alice opens another site. That site's page can contain a form whose action is the bank's transfer URL, and it can submit that form itself. The browser sends the request to the bank and, by the old rules, attaches the bank's cookie, because the request goes to the bank. The bank sees a valid session and moves the money. This is cross-site request forgery (CSRF): the attacker never sees the cookie or the response, it only makes Alice's browser send a request she did not mean to send.
The canvas shows Alice's browser (the sites she visits, the open tab, the page's code, her cookie jar and the browser's cookie decision), the request and response on the wire, and the bank's server (its session store, its checks and Alice's account). The table at the bottom keeps every request. Pick a request and press Send; the two selects change the cookie's SameSite attribute and the bank's own defence.
Why it works
Two things make CSRF possible. Cookies are ambient credentials: the browser attaches them automatically, based on where the request goes, not on which page started it. And some requests are allowed across sites without any permission: a form submission, a link, an image. The same-origin policy stops the attacker's page from reading the bank's answer, but not from sending the request (Demo 1: no defence, the form moves $100). CORS does not help either: it controls reading responses, and a plain form POST needs no preflight (see CORS and the Preflight Request).
SameSite: the browser leaves the cookie out
The SameSite cookie attribute tells the browser when to attach the cookie to a request that comes from another site:
| value | cross-site form POST | cross-site link (top-level GET) | same-site request |
|---|---|---|---|
None (needs Secure) | sent | sent | sent |
Lax | not sent | sent | sent |
Strict | not sent | not sent | sent |
| not set (Chrome) | like Lax, but sent during the cookie's first 2 minutes | sent | sent |
With Lax the hidden form arrives without the cookie, so the bank treats it as an anonymous visitor (Demo 2). But Lax still sends the cookie on a top-level GET navigation, because that is what following a link is. A bank that changes state on GET /transfer?to=… is still exploitable with a simple link (Demo 3). The rule behind it: never change state on GET.
Strict stops the link too, but it also drops the cookie when Alice follows a genuine link from her email, so she arrives logged out (Demo 4). A common setup is a Lax session cookie plus a Strict cookie that sensitive actions require.
Browsers now treat a cookie with no SameSite attribute as Lax. Chrome makes one exception: for the first two minutes after the cookie is set, a top-level cross-site POST still carries it, so login flows that POST back from an identity provider keep working. Inside that window the hidden form works again (Demo 5: no SameSite: 2-minute Lax+POST). Set the attribute explicitly.
Site is not origin
An origin is scheme + host + port: https://blog.bank.example and https://bank.example are different origins. A site is the scheme plus the registrable domain (eTLD+1): both are the site bank.example. SameSite looks at sites, so a request from any subdomain is same-site and carries the cookie even with Strict. If an attacker can put a page on a subdomain (a user blog, a forgotten staging host, a subdomain takeover), SameSite does not protect the bank at all (Demo 6: a same-site attacker (subdomain)).
The server's own defences
SameSite is a browser feature; the server should check too. The classic defence is a synchronizer token: the bank stores a random csrf_token in the session and writes it into a hidden field of its own forms. A state-changing request must send it back. The attacker's page cannot read it, because reading the bank's pages from another origin is exactly what the same-origin policy forbids, and that includes a subdomain (Demo 7: a CSRF token stops every forgery). The bank's own form carries the token and still works.
A double-submit cookie is the stateless variant: the token is in a cookie and must also appear in the request body; the server only compares the two. It is weaker against a subdomain attacker, who may be able to set a cookie for the whole site (cookie tossing), unless the token is signed and bound to the session.
An Origin check is cheaper: browsers send an Origin header on every cross-origin POST, and the server refuses state-changing requests whose origin is not its own (falling back to Referer when Origin is absent). Because it compares exact origins, it stops the subdomain attacker that SameSite let through (Demo 8). Newer browsers also send Sec-Fetch-Site: cross-site / same-site / same-origin, which the request panel shows; a server can refuse anything but same-origin for state changes.
| defence | where | stops cross-site form | stops same-site subdomain | cost |
|---|---|---|---|---|
| SameSite=Lax | browser | yes (POST) | no | state-changing GETs stay open |
| SameSite=Strict | browser | yes | no | links from other sites arrive logged out |
| synchronizer token | server | yes | yes | a token in every form and AJAX call |
| Origin / Referer check | server | yes | yes | must handle a missing header |
What is and is not affected
CSRF needs ambient credentials. An API that only accepts a token in an Authorization header (a bearer JWT kept in JavaScript) is not exposed to it, because another site cannot make the browser add that header; it trades CSRF for a larger XSS risk instead (see JWT vs Session Cookies). And any cross-site scripting bug defeats every CSRF defence, because script running on the bank's own origin can read the token and send same-origin requests. OAuth's state parameter is the same idea as a CSRF token, applied to the login redirect (see OAuth). Cookie attributes in general are on How Cookies Work.
See also HttpOnly Cookie vs localStorage: a token in localStorage is never sent by a cross-site form, but an XSS script can read it.
See also SSRF: the server-side cousin of CSRF, where the attacker borrows the server's network position instead of the user's cookies.
What the page leaves out
JSON APIs that only accept application/json (a form cannot send it without a preflight), login CSRF, clickjacking, the Public Suffix List that defines sites, schemeful same-site (http and https are different sites), how each browser rolled out Lax by default, and frameworks' built-in token handling.