SameSite Cookies

Chapter 7
SameSite Cookies
Lax / Strict / None, the browser-default shift, and what it does & doesn't cover

Tokens (Chapters 5–6) defend by adding a secret to the request. SameSite attacks CSRF from a different angle — precondition #3 (Chapter 1): it tells the browser not to send the cookie cross-site in the first place. It's a one-line cookie attribute that, combined with a 2020 change to browser defaults, reshaped the entire CSRF landscape.

The Attribute

SameSite is set on a cookie and controls whether the browser attaches it to cross-site requests. It has three values:

Set-Cookie: session=abc; SameSite=Strict; Secure; HttpOnly Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly Set-Cookie: session=abc; SameSite=None; Secure; HttpOnly # Secure is REQUIRED for None
ValueCookie sent on same-site requests?Sent on cross-site requests?
StrictYesNever — not even top-level navigation
LaxYesOnly on top-level GET navigations (clicking a link)
NoneYesYes (legacy behaviour) — requires Secure

How Each Value Affects CSRF

  • Strict — the cookie is never sent on any cross-site request, so forged requests arrive with no session cookie at all → CSRF fully blocked. The cost: even a legitimate inbound link from another site (e.g. an email link to your dashboard) arrives logged-out, which hurts UX.
  • Lax — the cookie is sent on top-level GET navigations (so following a link to your site keeps you logged in) but not on cross-site POSTs, forms targeting your site, or background subresource requests. This blocks the classic POST-form CSRF while preserving normal link UX — the sweet spot, and now the default.
  • None — the old behaviour: cookie sent on all cross-site requests. This is what CSRF exploited. Now requires Secure, and is only appropriate for cookies that genuinely need cross-site use (e.g. third-party embeds, SSO).
The 2020 default flip — the biggest single change to CSRF's threat model
Historically, a cookie with no SameSite attribute defaulted to None (send everywhere) — which is why CSRF was so easy. In 2020, Chrome (then other browsers) changed the default for cookies without an explicit SameSite to Lax. Overnight, the most common CSRF vector — a cross-site POST form — stopped carrying the session cookie by default, on most of the web, with no developer action. This is why CSRF is less dangerous today than a decade ago. But "less" is not "gone," and relying on the default alone is risky (see below).

Why Lax Stops the Classic Attack

Recall the canonical attack (Chapter 3): an auto-submitting hidden POST form on evil.com. With SameSite=Lax, the browser will not attach the session cookie to that cross-site POST, because Lax only sends cookies on top-level GET navigations — not POSTs, not subresource requests. The forged POST arrives unauthenticated, and the server treats it as a logged-out request. The hidden-form workhorse of CSRF is neutralized by a single cookie attribute.

Lax is not a complete defence — five gaps to know
Don't treat SameSite=Lax as "CSRF solved." It leaves real gaps: (1) State-changing GET endpoints — Lax does send the cookie on top-level GET navigations, so a forged GET (window navigation, not just an img) can still carry it (yet another reason GET must be safe). (2) The "Lax+POST" grace window — Chrome historically allowed cookies on cross-site POSTs for ~2 minutes after a cookie was set, to avoid breaking flows; edge cases linger. (3) SameSite=None cookies (needed for legitimate cross-site use) get no protection. (4) Same-site, different-origin — SameSite is about site (registrable domain), not origin, so a.example.com attacking b.example.com is "same-site" and not blocked. (5) Older browsers may ignore it. So use SameSite and tokens — defence in depth (Chapter 8).

"Site" vs "Origin" — a Crucial Distinction

SameSite operates on the concept of site (the registrable domain, e.g. example.com), not origin (scheme + host + port). This is looser than the Same-Origin Policy:

Request from → toSame-origin?Same-site?
app.example.comapp.example.comYesYes
blog.example.comapp.example.comNoYes (same registrable domain)
evil.comapp.example.comNoNo

The middle row is the catch: a sibling subdomain is same-site, so SameSite won't stop a malicious or compromised subdomain — which ties directly to the cookie-injection concern from Chapter 6. SameSite defends the cross-site boundary, not the cross-origin-but-same-site one.

Practical Recommendation

For a typical session cookie: SameSite=Lax (the default, but set it explicitly), plus Secure and HttpOnly. Use Strict for the highest-sensitivity cookies where the "arrive logged out from external links" cost is acceptable (or pair Strict with a Lax "read" cookie). Reserve None for cookies that truly require cross-site sending, always with Secure. And — the recurring theme — layer it with anti-CSRF tokens rather than relying on SameSite alone.

Hands-On Exercises

Exercise 1

In DevTools (Application → Cookies), inspect the SameSite value of cookies on a few sites. Then explain, for a SameSite=Lax session cookie, exactly which of these carry it: (a) clicking a link from another site, (b) a cross-site auto-submitting POST form, (c) a cross-site <img> GET.

📄 View solution
Exercise 2

Explain how the 2020 change of the default SameSite value (None → Lax) reduced CSRF risk across the web without developers doing anything. Then give two reasons you should still set SameSite explicitly and still use tokens.

📄 View solution
Exercise 3

A developer sets SameSite=Lax and declares the app "CSRF-proof." Identify the gaps that leave it still attackable — covering state-changing GETs, the site-vs-origin distinction (sibling subdomains), and SameSite=None cookies — and state what to add to actually close them.

📄 View solution

Chapter 7 Quick Reference

  • SameSite — cookie attribute controlling whether the cookie is sent on cross-site requests (attacks CSRF precondition #3)
  • Strict — never sent cross-site (blocks CSRF fully; external links arrive logged-out)
  • Lax — sent only on top-level GET navigations; blocks cross-site POST forms while keeping link UX (the default & sweet spot)
  • None — sent on all cross-site requests (legacy CSRF-enabling behaviour); now requires Secure
  • 2020 default flip (None → Lax) hugely reduced CSRF across the web automatically — but "less" ≠ "gone"
  • Gaps: state-changing GETs · Lax+POST grace window · SameSite=None cookies · sibling subdomains · old browsers
  • Site ≠ origin — SameSite uses the registrable domain, so a.example.comb.example.com is "same-site" and unprotected
  • Recommendation: session cookie = Lax + Secure + HttpOnly (Strict for high-sensitivity), and tokens — defence in depth
  • Next chapter: defence in depth — custom headers, Origin/Referer checks, re-authentication, combining layers