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

The Cookie Model

Cookies are scoped by domain, not origin: who can set one, who receives one, and what __Host-, Secure, Path, and Partitioned actually change.

Cookies predate the origin, and it shows

Every other web security mechanism is scoped to an origin — the triple of scheme, host, and port. Cookies are not. They were designed before the same-origin policy and are scoped to a domain, ignoring both scheme and port.

This single mismatch is the source of most cookie-related security surprises:

  • https://app.example.com and http://app.example.com are different origins but share cookies (except Secure ones).
  • https://app.example.com:8443 and https://app.example.com are different origins but share cookies entirely — port is not part of cookie scope.
  • evil.example.com can set a cookie that app.example.com will receive, if it sets Domain=example.com.

That last one is the important one, and it is why Subdomain Cookie Injection works despite SameSite. A cookie is not owned by the origin that set it.

"Site" is not "origin"

SameSite says site, deliberately. A site is the registrable domain: the public suffix plus one label, as determined by the Public Suffix List.

ABSame origin?Same site?
https://app.example.comhttps://app.example.comYesYes
https://app.example.comhttps://api.example.comNoYes
https://app.example.comhttps://evil.example.comNoYes
https://app.example.comhttp://app.example.comNoNo — schemeful same-site
https://app.example.comhttps://app.example.com:8443NoYes
https://a.github.iohttps://b.github.ioNoNo — github.io is a public suffix

Read the third row again. A sibling subdomain is same-site. Every SameSite protection — Lax, Strict, all of it — treats a request from evil.example.com to app.example.com as same-site and sends the cookie. If an attacker controls any subdomain, SameSite has been bypassed without any technique at all.

The public-suffix row is the reason shared hosting on *.github.io, *.pages.dev, and similar is safe by default: those suffixes are on the PSL, so each subdomain is its own site.

The attributes that matter

AttributeWhat it doesWhat it does not do
HttpOnlyHides the cookie from document.cookie, so XSS cannot read it directly.Nothing for CSRF. The browser still attaches the cookie automatically — which is the entire problem. HttpOnly and CSRF are orthogonal.
SecureOnly sent over HTTPS. Required for SameSite=None and for the __Host- and __Secure- prefixes.Does not stop a plaintext subdomain from OVERWRITING the cookie — a non-Secure cookie of the same name set over http can shadow it, unless __Host- is used.
DomainWidens scope to a parent domain and all its subdomains. Domain=example.com is received by every *.example.com.There is no way to narrow scope below the host, and no way to exclude a subdomain. Omitting Domain is the safe choice: the cookie is then host-only.
PathRestricts which paths receive the cookie.Not a security boundary. Any page on the origin can read cookies from any path via an iframe, and path does not participate in the overwrite rules in a useful way.
SameSiteControls whether the cookie rides a cross-SITE request. See the dedicated guide.Nothing against a same-site attacker, nothing against non-cookie credentials, and nothing against a state-changing GET under Lax.
Partitioned (CHIPS)Puts the cookie in a jar keyed by the top-level site, so an embedded context gets its own isolated copy.Only relevant to third-party contexts. Does not affect top-level navigation, which is where most live CSRF now lives.

The __Host- prefix is the underused one

HTTPVulnerable
Set-Cookie: csrf_token=8f3a...; Domain=example.com; Path=/; Secure

# Domain=example.com means every subdomain both RECEIVES this cookie and can
# OVERWRITE it. If any *.example.com is compromised, hosts user content, or is
# a stale CNAME someone else can claim, an attacker sets their own csrf_token
# and the double-submit check compares attacker value to attacker value.
#
# It passes. SameSite does not help: the subdomain is same-site.

Why overwriting is the sharp edge

When the browser decides whether an incoming Set-Cookie replaces an existing cookie, it matches on name, domain, and path — and it does not consider the Secure flag or the scheme of the setter as part of that identity. Two consequences:

  1. A cookie set by evil.example.com with Domain=example.com and the same name occupies the same slot as the legitimate one, and the browser may send both.
  2. When two cookies of the same name are in scope, they are sent in one header — Cookie: csrf=attacker; csrf=legit — and the server's parser picks one, usually the first. The attacker controls the ordering by manipulating path specificity.

This is the mechanic behind cookie tossing, and it explains why naive double-submit is fragile: the defence assumes the cookie value is attacker-unreadable and attacker-unwritable, and only the first is true. See Double-Submit Cookies and Subdomain Cookie Injection.

Cookies are not the only ambient credential

The word to hold onto is ambient: a credential the browser attaches on its own, without the page asking. Cookies are the common case but not the only one, and the others are governed by no cookie attribute at all:

  • HTTP Basic and Digest — once cached for a realm, re-sent automatically.
  • NTLM / Negotiate (Kerberos) — transparently negotiated on intranets, which is why Windows Integrated Auth applications are so exposed.
  • TLS client certificates — selected once, then reused for the origin.

All three ignore SameSite entirely. An <img> tag pointed at an intranet host protected by NTLM still authenticates in 2026.

And the negative case, which decides half of all triage arguments: Authorization: Bearer is not ambient. The browser never adds it; the application's own JavaScript does. An API authenticated purely by bearer token cannot be attacked this way. If you find yourself writing a CSRF report against such an endpoint, the finding is something else.