Skip to content

Cheatsheet

The behavioural source of truth for this site. Every cell below is checked against the PoC generator at build time — if the table and the generator ever disagree about a verdict, the build fails.

Does the cookie ride along?

Rows are how an attacker's page makes the request. Columns are the target's session cookie policy. Depends means the engines disagree — read the note, and name the browser in your finding.

Request made from a cross-site pageSameSite=None; SecureSameSite=LaxSameSite=StrictNo SameSite attribute
Top-level GET navigation — location =, window.open, scripted <a>.click()SentSentBlocked

Strict is never sent on a cross-site request, including a navigation the user initiated.

Sent
<form method="GET"> submitted top-levelSentSentBlockedSent
<form method="POST"> submitted top-levelSent

A top-level navigation makes the target the first party, so no third-party cookie policy applies.

Blocked

Lax needs a top-level navigation AND a safe method. POST is not safe.

BlockedDepends

Chromium blocks it via the Lax default, except within the 120-second Lax+POST window for a freshly-set cookie. Firefox and Safari never shipped a Lax default and send it unconditionally. This is the single most misreported cell in CSRF findings.

<form method="POST" target="hidden-iframe">Depends

Chromium sends it; Firefox partitions it under Total Cookie Protection; Safari blocks third-party cookies outright.

BlockedBlockedBlocked

Submitting into a frame is a subresource request, not a navigation — so it loses the one shape that still carries an unset cookie.

<img>, <script>, <link rel=stylesheet>, CSS url()Depends

Chromium sends it; Firefox partitions it; Safari blocks it.

BlockedBlockedBlocked

Dead in all three engines, for three different reasons: Chromium's Lax default, Firefox's Total Cookie Protection, Safari's third-party blocking.

<iframe src="…">Depends

Chromium sends it; Firefox partitions it; Safari blocks it. This is why a framed target renders logged out — and why clickjacking-assisted CSRF now needs SameSite=None or a popup.

BlockedBlockedBlocked
fetch(url) — default credentialsBlocked

fetch defaults credentials to 'same-origin', so no cookie is attached at any SameSite value. Same for XHR without withCredentials.

BlockedBlockedBlocked
fetch(url, {mode:'no-cors', credentials:'include'})Depends

Chromium sends it; Firefox partitions it; Safari blocks it.

BlockedBlockedBlocked
XMLHttpRequest with withCredentials = trueDepends

Identical cookie behaviour to fetch. The differences are the preflight for non-simple shapes and an unreadable response without Access-Control-Allow-Origin.

BlockedBlockedBlocked
navigator.sendBeacon(url, blob)Depends

Credentials mode is 'include' by spec, but it is still a subresource, so Lax and Strict block it regardless.

BlockedBlockedBlocked
new WebSocket('wss://target/…') handshakeDepends

The handshake is a subresource GET with no CORS, so cross-site WebSocket hijacking requires SameSite=None. Chromium sends it; Firefox partitions it; Safari blocks it.

BlockedBlockedBlocked
Any request initiated from the target's own origin (on-site open redirect, client-side redirect gadget)SentSentSent

Not cross-site, so SameSite does not engage at all. This is exactly how an on-site gadget defeats SameSite=Strict.

Sent

What this table does not cover

  • HTTP Basic, Digest, NTLM/Negotiate, and TLS client certificates are ambient but are not cookies, so no cookie attribute governs them. Every row reads “sent”. See Routers, IoT, and localhost.
  • Authorization: Bearer is never ambient — the browser does not attach it. Token-in-header APIs are not CSRF-able by anything on this page.
  • A Partitioned (CHIPS) cookie lives in a jar keyed to the top-level site, so the attacker's page gets an empty partition and every third-party row reads “blocked”.
  • Schemeful same-site: http://target and https://target are cross-site to each other. Match the scheme in your PoC.
  • Chromium columns assume a default profile. Incognito blocks third-party cookies, and so do many users' settings and extensions.
  • A same-site attacker — anything on a sibling subdomain — is not cross-site at all, so none of these restrictions apply to them. See Subdomain Cookie Injection.

What each framework does by default

Column headers link to the full prevention guide.

Django 5.xRails 8Spring Security 6Laravel 11/12ASP.NET Core 9Express 5 / NodeGo (net/http)
On by defaultYes — CsrfViewMiddleware is in the default MIDDLEWARE listYes — default_protect_from_forgery since 5.2Yes — CsrfFilter is on unless you disable itYes — for the web middleware groupRazor Pages and MVC views yes; Web API controllers noNo — nothing is built inNo, but net/http ships CrossOriginProtection since Go 1.25
PatternMasked double-submit; synchronizer with CSRF_USE_SESSIONSSynchronizer, session-stored, XOR-maskedSynchronizer (HttpSessionCsrfTokenRepository)Synchronizer plus an XSRF-TOKEN mirror cookieEncrypted token pair, bound to the authenticated userWhatever you installFetch metadata (Sec-Fetch-Site), or gorilla/csrf for tokens
Token storagecsrftoken cookie, or the sessionSessionSession, or an XSRF-TOKEN cookieSession plus an encrypted cookie.AspNetCore.Antiforgery.* cookie— (header-based), or a cookie with gorilla/csrf
Form helper{% csrf_token %}form_with and csrf_meta_tagsThymeleaf injects _csrf automatically@csrfThe form tag helper injects it automaticallycsrf.TemplateField
AJAX headerX-CSRFTokenX-CSRF-TokenX-CSRF-TOKEN or X-XSRF-TOKENX-CSRF-TOKEN or X-XSRF-TOKENConfigurable via options.HeaderNameConvention onlyX-CSRF-Token
Origin / Referer checkYes — Origin against CSRF_TRUSTED_ORIGINS since 4.0, Referer fallback on HTTPSYes — forgery_protection_origin_checkNoNoNoNoYes — Sec-Fetch-Site with an Origin fallback
Methods treated as safeGET HEAD OPTIONS TRACEGET HEADGET HEAD TRACE OPTIONSGET HEAD OPTIONSGET HEAD OPTIONS TRACEGET HEAD OPTIONS
Session cookie SameSite defaultLax (SESSION_COOKIE_SAMESITE)Lax since 5.2Unset — server.servlet.session.cookie.same-site has no defaultlax (config/session.php)Lax on the auth cookie; the antiforgery cookie itself is StrictUnset — express-session sets nothingUnset — you set it yourself
Escape hatch@csrf_exemptskip_forgery_protection, or with: :null_session.csrf(c -> c.ignoringRequestMatchers(...)) or .disable()validateCsrfTokens(except: [...])[IgnoreAntiforgeryToken]AddTrustedOrigin, or simply not wrapping the mux
Biggest footgun@csrf_exempt on API views; DRF SessionAuthentication is checked but TokenAuthentication skips CSRF entirelywith: :null_session on a controller that still uses cookie sessions — the request proceeds with an empty session instead of failing.csrf().disable() copied from a stateless-JWT tutorial into a cookie-session app; withHttpOnlyFalse() degrades it to plain double-submitWildcards in the except list; Sanctum SPA mode with a wrong SANCTUM_STATEFUL_DOMAINS[ApiController] plus cookie auth means no antiforgery runs at allcsurf was deprecated and archived in 2022; express.json({type:'*/*'}) reopens the text/plain vectorForgetting to wrap the mux; gorilla/csrf without TrustedOrigins behind a proxy
Recommended 2026 baselineKeep the defaults, add a __Host- prefix and SameSite=LaxKeep the defaultsCookieCsrfTokenRepository + XorCsrfTokenRequestAttributeHandler, plus your own Origin checkKeep the defaults and keep except empty[AutoValidateAntiforgeryToken] globally, including API controllerscsrf-csrf (signed double-submit) plus a Sec-Fetch-Site rejectionhttp.NewCrossOriginProtection() wrapping the mux