SameSite Semantics
Exactly when Lax, Strict, None, and an absent attribute attach a cookie to a cross-site request — and why the three engines disagree.
Two questions, not one
SameSite is the cookie attribute that decides whether a cookie rides along on a cross-site request. Reading it correctly means answering two independent questions about the request, not one:
- Is it a top-level navigation, or a subresource? A navigation replaces the whole page — clicking a link, submitting a form,
location = ...,window.open. A subresource is anything loaded by a page:<img>,<iframe>,<script>,fetch, XHR, a WebSocket handshake. - Is the method safe?
GETandHEADare safe in the fetch-spec sense.POSTis not — and note this is about the method's classification, not about whether it has a body or whether it is idempotent.
Lax requires both: a top-level navigation and a safe method. Almost every mistake people make with SameSite comes from remembering only one of the two halves.
The site in SameSite is also not the origin — see The Cookie Model. evil.example.com and app.example.com are the same site, which is why SameSite does nothing against subdomain cookie injection.
The four values
The full behaviour matrix
The complete request-shape by cookie-value grid — thirteen request shapes across all four values, per engine — lives on the Cheatsheet. It is the machine-checked source of truth for this site: the build fails if the generator on the PoC page and that table ever disagree.
The rows worth memorising:
- Top-level GET navigation carries the cookie under
None,Lax, and absent. OnlyStrictblocks it. This is why an endpoint that changes state on GET gets no protection from SameSite at all. - Top-level POST form carries it under
Noneeverywhere, and under absent in Firefox and Safari. Chromium blocks it outside the Lax+POST window. - Every subresource —
<img>,<iframe>,fetch,sendBeacon, WebSocket — carries it only underNone, and even then only in Chromium.
Setting it correctly
Set-Cookie: session=abc123; Path=/; HttpOnly
# No SameSite attribute. Chromium treats it as Lax; Firefox and Safari do not.
# The application's CSRF posture now differs by browser, which means the team
# cannot reason about it and testers will get contradictory results.
Set-Cookie: session=abc123; Path=/; SameSite=None; Secure; HttpOnly
# Explicitly opting out. Sometimes genuinely required (embedded widgets, a
# third-party API), but it restores the full pre-2020 attack surface and must
# be paired with a token — SameSite is no longer doing anything for you.What SameSite does not cover
SameSite is a useful default-deny layer. It is not a CSRF defence on its own, and these are the gaps — every one of them has its own guide:
- Same-site attackers. A subdomain is same-site. If any subdomain can be taken over, or hosts user content, or is a stale CNAME, SameSite is bypassed entirely. See Subdomain Cookie Injection.
- State-changing GET.
Laxsends the cookie on a top-level GET navigation by design. If the endpoint acts on GET, nothing has been prevented. - On-site gadgets. An open redirect on the target's own origin launders a cross-site request into a same-site one, defeating even
Strict. See Bypassing SameSite. - Non-cookie ambient credentials. Basic auth, NTLM, and TLS client certificates are not cookies. No cookie attribute applies to them. See Routers, IoT, and localhost.
SameSite=Noneby necessity. Plenty of applications need it. Those are back to 2019 and need a real token.- Browser disagreement. An absent attribute means your protection is Chromium-only.
The recommendation is unchanged from OWASP's: treat SameSite as defence in depth underneath a token, not as a replacement for one. See Defense in Depth.
Schemeful same-site and partitioning
Two refinements that change verdicts in practice:
Schemeful same-site. http://app.example and https://app.example are cross-site to each other under the modern definition. A PoC served over plaintext against an HTTPS target will not reproduce, and a Secure cookie would not be sent over the plaintext one anyway. Always match the scheme.
CHIPS / Partitioned. A cookie set with the Partitioned attribute lives in a jar keyed by the top-level site. Embedded in attacker.example, the target gets a fresh, empty partition — so every third-party row reads "blocked" regardless of SameSite=None. This is the intended replacement for third-party cookies and, as a side effect, closes the None + subresource vector for cookies that adopt it.
Firefox's Total Cookie Protection applies the same partitioning idea to all third-party cookies without requiring the attribute, which is why Firefox blocks subresource CSRF even though it has no Lax default.
Related
Cookies are scoped by domain, not origin: who can set one, who receives one, and what __Host-, Secure, Path, and Partitioned actually change.
What still works and what died with Lax-by-default — the honest, browser-by-browser exploitability verdict, plus the techniques that are now purely historical.
Lax top-level GET, Chromium's 120-second Lax+POST window, on-site redirect gadgets against Strict, and schemeful downgrade.
Overwriting the CSRF cookie from a sibling subdomain, why SameSite offers zero protection here, and why __Host- is the fix.
img, script, link, scripted link clicks, and top-level navigation — and why only the last one still carries a cookie.
The layering order — SameSite=Lax, __Host- prefix, Fetch Metadata rejection, then a token — plus re-authentication for high-value operations.
Top-level form navigation reaches private addresses that Local Network Access does not gate, and Basic/NTLM/client-cert auth is ambient and SameSite-immune.