All posts

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-site fetch, 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 requires None to be paired with Secure; Chromium and Firefox reject a None cookie 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 pathSameSite layerCSRF token layer
Cross-site POST from a hidden formBlocked by Lax and StrictBlocked (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 stateAllowed under LaxBlocked (no valid token)
Method override (_method) behind a top-level GETAllowed under LaxBlocked (no valid token)
Forged request from a sibling subdomainNot blocked (same site)Blocked (no valid token)
User on Safari or Firefox with an attribute-less cookieNot blocked (no Lax default)Blocked (no valid token)
Attribute-less cookie in Chromium's two-minute POST windowNot blockedBlocked (no valid token)
Cookie theft via XSSNot its jobNot 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 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.

Sources