Skip to content
CWE-1275A01:2021 – Broken Access Control

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:

  1. 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.
  2. Is the method safe? GET and HEAD are safe in the fetch-spec sense. POST is 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

ValueCookie is sent cross-site when…Notes
NoneAlways — subject to the browser's third-party cookie policyRequires the Secure attribute; a None cookie without Secure is rejected outright. This is the value that keeps classic CSRF alive, and apps set it whenever they need embedding or third-party API access.
LaxTop-level navigation AND a safe method (GET/HEAD)Blocks cross-site POST, and blocks every subresource including cross-site GET images and iframes. The Lax+POST intervention does NOT apply to an explicit Lax.
StrictNeverAlso blocks the cookie when a user clicks a genuine link from another site, which is why apps avoid it for session cookies — the user lands logged out. Defeated by an on-site gadget, not by a cross-site request.
AbsentDepends on the browserChromium treats it as Lax (plus the 120-second Lax+POST window). Firefox and Safari do not — they apply no SameSite default and rely on third-party cookie partitioning or blocking instead.

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. Only Strict blocks 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 None everywhere, 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 under None, and even then only in Chromium.

Setting it correctly

HTTPVulnerable
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. Lax sends 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=None by 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.