SameSite vs CSRF tokens, does the cookie attribute replace the token
CentralCSP Team ·
Last update:
No. SameSite does not replace CSRF tokens. The cookie attribute is real
defense in depth against cross-site request forgery, but it is site-scoped, the
browser default is not applied
everywhere, and its relaxed mode still lets some cross-site requests through.
Even the cookie specification says Lax enforcement is not a complete CSRF
defense and points developers to a session management mechanism for full
coverage. Set SameSite
explicitly and keep your CSRF tokens. This post explains what each layer blocks,
the four documented ways around SameSite, and why the two defenses cover each
other's gaps.
What CSRF is, and why cookies make it possible
Cross-site request forgery (CSRF) is an attack where a page the attacker controls makes the victim's browser send a state-changing request to your site. The victim is already logged in, so the request carries their session and the server acts on it: transfer money, change an email, delete an account.
The reason this works at all is that the browser attaches your cookies to a
request automatically, based on where the request is going, not on where it came
from. An attacker's page cannot read your session cookie, but it can trigger a
request to your site (a form auto-submit, an image load, a fetch), and the
browser helpfully includes the session cookie anyway. That automatic attachment
is the whole vulnerability. Both defenses below exist to break it.
What SameSite blocks, and what Lax still allows
SameSite is a Set-Cookie attribute
that tells the browser when a cookie may ride along on cross-site requests. It has
three values.
SameSite=Strict: the cookie is sent only on same-site requests. A request originating from any other site never carries it, including a normal link click from an email or another site. The session is intact, but that first navigation arrives without the cookie, so the page renders as if the user were logged out.SameSite=Lax: the cookie is sent same-site, plus on cross-site top-level navigations that use a safe method (a GET link click or a GET form). A top-level navigation is one that changes the address bar, so a cross-sitefetch, an<img>, a<script>, or an<iframe>request never carries the cookie.SameSite=None: the cookie is sent on every request, cross-site included. The cookie specification requiresNoneto be paired withSecure; Chromium and Firefox reject aNonecookie without it, while Safari currently accepts it.
Lax stops the most common CSRF vector, the hidden cross-site POST, because a cross-site POST is not a safe top-level navigation. What Lax still allows is the cross-site top-level GET. If a GET request on your site changes state (it should not, but many do), Lax will not stop an attacker navigating the victim's browser to it.
Note also what SameSite never touches: same-site requests. All three values only
gate cross-site sending. Any request that is already same-site, including a
JavaScript-initiated fetch from any page on your site, carries the cookie in
full. That matters below.
The gaps that keep tokens necessary
SameSite narrows the attack surface. It does not close it, for four documented
reasons. These match the bypass classes PortSwigger catalogs for SameSite
restrictions.
SameSite is site-scoped, not origin-scoped. A "site" in the cookie spec is
the scheme plus the registrable domain, so https://a.example.com and
https://b.example.com are the same site, while http://example.com and
https://example.com are not. A cookie set with any SameSite value is sent on
requests originating from any sibling subdomain. PortSwigger's phrasing is worth
remembering: a request can still be same-site even if it is issued cross-origin.
An XSS bug or an open redirect on any subdomain of your registrable domain
becomes a same-site gadget; requests launched from it carry all cookies for the
site, and SameSite contributes nothing. A CSRF token still requires the
attacker to obtain the token value. (Full XSS on the same origin as the form can
usually read the token too, so tokens mitigate the sibling-subdomain case more
than same-origin XSS; XSS needs its own defenses, starting with a
Content Security Policy.)
The browser default is not applied everywhere. Chromium-based browsers treat
a cookie with no SameSite attribute as Lax. Firefox shipped that behavior only
behind a flag, never in a release, and disabled even its Nightly experiment in
2023. Safari applies no default at all. In those browsers an attribute-less
session cookie rides on every cross-site request, exactly the pre-2020 behavior.
Relying on the default means relying on which browser your user happens to run.
Set the attribute yourself.
Attribute-less cookies get a two-minute cross-site POST exception in Chromium.
When Chromium defaults a cookie to Lax, it still sends that cookie on cross-site
top-level POST requests for the first two minutes after the cookie is created,
an intervention announced as temporary in 2019 and still shipping in Chromium's
source in 2026. PortSwigger documents attacks that reopen the window by forcing
the victim's browser to fetch a fresh session cookie, then firing the forged
top-level POST within 120 seconds. The exception applies only to cookies with no
attribute; a cookie set with an explicit SameSite=Lax never gets it. One more
reason to set the value rather than lean on the default.
A GET that changes state is still exposed under Lax. Lax permits cross-site
top-level GET navigations by design. That covers not only endpoints that misuse
GET, but also frameworks that honor a method override parameter (Symfony's
_method, for example), which let a Lax-permitted GET reach a server-side POST
route. A token check does not care about the method, so it covers both cases.
Why HttpOnly does not help against CSRF
HttpOnly is worth setting, but it solves a different problem. It stops JavaScript
from reading the cookie (document.cookie), which blocks a cross-site scripting
(XSS) payload from stealing the session value. It does nothing against CSRF,
because CSRF never needs to read the cookie. The browser still attaches an
HttpOnly cookie to the forged request automatically, which is exactly the
behavior the attack abuses.
SameSite vs CSRF tokens, what each layer blocks
CSRF tokens use a value the attacker cannot supply. The server generates an
unpredictable token, embeds it in the page or form, and rejects any state-changing
request that does not echo it back. This is OWASP's primary recommendation for
stateful applications (the synchronizer token pattern), with SameSite treated
as a defense-in-depth layer on top, not a replacement. An attacker's cross-site
page cannot read the token (the same-origin policy blocks it), so it cannot forge
a request that passes the check.
| Attack path | SameSite layer | CSRF token layer |
|---|---|---|
| Cross-site POST from a hidden form | Blocked by Lax and Strict | Blocked (no valid token) |
Cross-site subresource request (fetch, img, script) | Blocked by Lax and Strict (not a top-level navigation) | Blocked (no valid token) |
| Cross-site top-level GET that changes state | Allowed under Lax | Blocked (no valid token) |
Method override (_method) behind a top-level GET | Allowed under Lax | Blocked (no valid token) |
| Forged request from a sibling subdomain | Not blocked (same site) | Blocked (no valid token) |
| User on Safari or Firefox with an attribute-less cookie | Not blocked (no Lax default) | Blocked (no valid token) |
| Attribute-less cookie in Chromium's two-minute POST window | Not blocked | Blocked (no valid token) |
| Cookie theft via XSS | Not its job | Not its job |
The pattern is consistent: SameSite is a good outer wall that some requests walk
around, and the token is the check that does not depend on request origin, request
method, or browser behavior.
Set the cookie, and keep the token
Set SameSite explicitly on your session cookie, alongside Secure and
HttpOnly, using the host-only __Host- prefix for a session ID.
Set-Cookie: __Host-session=8f3k...; Secure; HttpOnly; SameSite=Lax; Path=/Use Strict if landing logged-out after an external link is acceptable; use
Lax when you need inbound links to keep the session. Then keep a token on
every state-changing request. The token lives in the form or a request header, and
the server validates it server-side.
<form method="POST" action="/account/email">
<input type="hidden" name="csrf_token" value="{{ server_generated_token }}">
<input type="email" name="email">
<button type="submit">Save</button>
</form>Recommendation
Set SameSite explicitly (Strict preferred, Lax when you need cross-site
top-level GETs to keep the session) and never rely on the browser default. Set
Secure and HttpOnly too, and prefer the __Host- prefix for session IDs. On
top of that, keep CSRF tokens on every state-changing endpoint, which is the
OWASP baseline. SameSite shrinks the attack, the token closes the cases it
leaves open, and together they cover each other's gaps. Check the Set-Cookie
attributes you actually ship with CentralCSP's free
security headers scanner, which reports the SameSite,
Secure and HttpOnly flags on every cookie a page sets.
FAQ
Is SameSite enough to prevent CSRF?
No. OWASP classifies SameSite as a defense-in-depth control that does not
replace a dedicated CSRF defense, and the cookie specification itself says Lax
enforcement is not a complete defense against CSRF as a category. It is
site-scoped (sibling subdomains bypass it), not applied by default in Safari or
release Firefox, and Lax still permits cross-site top-level GETs.
Do I still need CSRF tokens if I set SameSite Lax?
Yes, for almost every app. OWASP's primary recommendation for stateful
applications is the synchronizer token pattern, with SameSite layered on top.
Tokens do not depend on browser defaults, site scoping, or request method, so
they cover every gap Lax leaves.
SameSite Lax vs Strict, which is better for CSRF?
Strict never sends the cookie cross-site, so it also blocks the state-changing-GET case, but users arriving from an external link land on a logged-out view. Lax keeps inbound links working and leaves the state-changing GET (or a method override behind one) forgeable. Neither value protects against an attacker who can issue same-site requests.
What are the known SameSite bypasses?
Four documented classes. Cross-site top-level GETs under Lax, including
framework method overrides. Chromium's two-minute cross-site POST window for
cookies with no SameSite attribute. Same-site gadgets, forging from a sibling
subdomain via XSS or an open redirect. And browsers with no Lax default
(Safari, release Firefox), which send an attribute-less cookie on every
cross-site request.
Related reading
- Cookie security: Set-Cookie attributes,
the reference for
Secure,HttpOnly,SameSite, and the cookie prefixes. - X-Frame-Options vs frame-ancestors, the other browser control for a cross-site attack (clickjacking).
- How to improve your security headers grade, the fix list for the full response-header set, cookies included.
- no-cache vs no-store, keeping the pages that carry the session out of shared caches.
Sources
- rfc6265bis (Cookies, HTTP State Management Mechanism), SameSite semantics, the site definition, Lax-allowing-unsafe, and the None-requires-Secure rule.
- Chromium SameSite updates, the Lax-by-default rollout and the two-minute Lax+POST intervention.
- Chromium cookie_constants.h, the two-minute allowance still present in source.
- MDN Set-Cookie reference,
SameSite,HttpOnly, and the__Host-prefix. - MDN browser-compat-data for Set-Cookie, per-browser Lax default and None-requires-Secure support.
- Mozilla bug 1812297, Firefox never shipped Lax-by-default in a release.
- OWASP CSRF Prevention Cheat Sheet, the synchronizer token pattern and the defense-in-depth position.
- OWASP Session Management Cheat Sheet, the session cookie baseline.
- PortSwigger, bypassing SameSite restrictions, the documented bypass techniques.