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

Defense in Depth

The layering order — SameSite=Lax, __Host- prefix, Fetch Metadata rejection, then a token — plus re-authentication for high-value operations.

No single control covers everything

Each CSRF defence has a specific gap, and the gaps do not overlap. That is the argument for layering — not vague caution, but the observation that the thing which defeats one layer is precisely what the next layer catches.

  • SameSite fails against a same-site attacker, against state-changing GET, and against non-cookie credentials.
  • A token fails against XSS and against a CORS misconfiguration that lets the attacker read it.
  • Header checks fail when the header is absent, when a proxy rewrites it, and against a same-site attacker.
  • Content-type enforcement fails to a single misconfigured body parser.

Stack them and each layer covers another's gap. The recommended order below is by cost-to-benefit: the first two are configuration changes, the third is a dozen lines of middleware, and only the fourth requires real work.

What each layer actually stops

AttackSameSite=Lax__Host- prefixFetch MetadataTokenRe-auth
Cross-site POST formStops itStops itStops itStops it
Cross-site GET navigationSends the cookieStops itStops itStops it
Subresource (img, iframe, fetch)Stops itStops itStops itStops it
Same-site attacker (subdomain)Sends the cookieAllows same-siteStops itStops it
Cookie tossing / double-submit overwriteNo effectStops itNo effectStops it if signedStops it
On-site redirect gadget vs StrictBypassedLooks same-originStops itStops it
text/plain JSON smuggleDepends on shapeStops itStops itStops it
CORS-assisted (token readable)Depends on shapeStops itBypassedStops it
Basic / NTLM / client certificateNo effectNo effectStops itStops itStops it
XSS on the target originNo effectNo effectNo effectBypassedBypassed
Clickjacking / popup consentStops the framed formLooks same-originBypassedStops it

Reading that table

Three things stand out, and they are the practical takeaways:

Re-authentication is the only column with no gaps except XSS. That is why it belongs on the operations that actually matter — password change, email change, adding a payment method, adding an API key, granting an OAuth scope, deleting an account. It is not a general CSRF defence because it cannot be applied to every request, but for the handful of actions that grant durable control it is the strongest thing available.

XSS defeats everything. Script running on the target's own origin reads the token, satisfies every header check, and is same-origin by definition. There is no CSRF control that survives it, which is why a CSRF finding on a page with XSS is redundant and why XSS should always be fixed first.

SameSite alone leaves three holes — GET, same-site attackers, and non-cookie credentials — and all three are common. Teams that adopted SameSite=Lax and removed their tokens made themselves less safe, not more.

The recommended stack

HTTPSecure
# Layers 1 and 2 are a single configuration change with no code.

Set-Cookie: __Host-session=eyJ...; Path=/; Secure; HttpOnly; SameSite=Lax

# SameSite=Lax explicitly -- not left absent. An absent attribute makes the
# behaviour Chromium-specific and opens the 120-second Lax+POST window.
#
# Lax rather than Strict: Strict logs users out when they arrive from an
# external link, which is why almost nobody ships it on a session cookie.
# Consider Strict for a SECOND cookie that gates high-value actions.
#
# __Host- is enforced by the browser and requires Secure, requires Path=/,
# and FORBIDS Domain. Host-only means a sibling subdomain cannot overwrite
# it -- the one thing SameSite cannot help with.

# Plus HSTS, so the plaintext overwrite route is closed too:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# And, while you are in the headers, the framing control:
Content-Security-Policy: frame-ancestors 'none'

Which operations get re-authentication

Re-authentication has a real usability cost, so apply it where the action grants durable control rather than everywhere. The test: if an attacker performed this once, would they retain access afterwards?

Yes, so require it:

  • Changing the email address or the recovery phone number
  • Changing the password
  • Adding or changing a payment method or payout destination
  • Creating an API key, personal access token, or SSH key
  • Linking a new identity provider or OAuth application
  • Adding a user to a team, or granting admin
  • Disabling MFA, or adding an MFA device
  • Deleting the account or exporting all data

No, a token is enough:

  • Ordinary content edits, preferences, and settings
  • Anything reversible and visible to the user

And pair the high-value list with out-of-band notification — an email to the previous address on an email change, with an undo link. That does not prevent the action, but it bounds the damage and is frequently the only control that catches a successful attack after the fact.

The whole checklist

Cookies

  • SameSite=Lax set explicitly on the session cookie
  • __Host- prefix, Secure, HttpOnly, no Domain
  • HSTS with includeSubDomains, preloaded
  • Session identifier regenerated on login and privilege change

Headers

  • Sec-Fetch-Site rejection for cross-site state changes, mounted globally
  • Missing-header behaviour decided deliberately and documented
  • Origin exact-matched against an allowlist where used; null never allowlisted
  • Content-Security-Policy: frame-ancestors 'none'

Token

  • Default-deny middleware, empty exemption list
  • Bound to the session, rejected when absent, constant-time comparison
  • Per-session, rotated on login
  • Never in a URL

Requests

  • GET, HEAD, OPTIONS are side-effect free
  • Content type enforced with a 415 on state-changing routes
  • Explicit verbs on every route; method override from a header only, applied before the CSRF check

High-value operations

  • Re-authentication required
  • Out-of-band notification sent, with an undo path

Subdomains

  • Enumerated, and dangling DNS records audited on a schedule
  • same-site treated as untrusted if any subdomain hosts user content