AppSec · Aug 11, 2026 · 7 min read

CSRF: confirming the state-change that token and SameSite both missed

A state-changing request that succeeds cross-origin without a valid CSRF token is confirmed forgery - a present-but-unvalidated token and SameSite=Lax gaps each need a separate confirming probe.

CSRF exploits the browser's automatic cookie-forwarding: any page a victim visits can trigger authenticated requests to any origin, because the browser delivers that origin's cookies regardless of which page made the request. A state-changing endpoint protected only by a session cookie cannot distinguish a request the application initiated from one a third-party page triggered. The confirming artifact is not a theoretical gap - it is a state-changing request submitted from a cross-origin context that the server accepts, with a before-and-after read of the target resource proving the change actually happened.

01The browser's implicit trust model

Browsers attach cookies to requests by destination origin, not by the origin of the page that made the request. A script on https://attacker.example can POST to https://bank.example/transfer, and the browser delivers the bank's session cookies alongside that request. The server receives a fully authenticated-looking request it did not generate. CSRF protection exists to prove the request came from the application's own context - not just that a valid session cookie was present.

victim browser (session cookie: session=abc123) | |--> GET https://evil.example/csrf.html victim clicks a link or visits a page | auto-submit form or fetch() fires immediately | `--> POST https://api.target.example/transfer browser attaches target cookies automatically Cookie: session=abc123 (no CSRF token sent) HTTP/1.1 200 OK - funds transferred, no protection fired
The browser delivers cookies to the target origin regardless of which page triggered the request. No CSRF control means every cross-origin page is a potential forger.

02Confirming CSRF without running a real attack

The confirming probe submits a state-changing request from a controlled cross-origin context - a test page the scan operator controls - and checks whether the server accepts it. The operation must be benign and reversible: updating a display preference, toggling a low-privilege setting, or incrementing a counter the scan operator can reset. No funds are transferred, no credentials changed, no records deleted. The probe establishes three facts in sequence: the state before the cross-origin request, the server's response to it, and the state after. All three are required to call the finding proven rather than reachable.

csrf-probe.httphttp
# Step 1: read baseline state from the authenticated endpoint
GET /api/profile/prefs HTTP/1.1
Origin: https://app.target.example
Cookie: session=abc123
HTTP/1.1 200 OK  {"theme":"light"}   # baseline: theme is light

# Step 2: cross-origin POST from a probe origin the server has never trusted
POST /api/profile/prefs HTTP/1.1
Origin: https://probe-62615533.invalid
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded

theme=dark

HTTP/1.1 200 OK   # cross-origin request accepted

# Step 3: confirm state changed - same authenticated GET as Step 1
GET /api/profile/prefs
HTTP/1.1 200 OK  {"theme":"dark"}   # state changed - CSRF proven
Proven - HighCSRF - no token validation - POST /api/profile/prefs
POST /api/profile/prefs Origin: https://probe-62615533.invalid Cookie: session=abc123 --> 200 OK (state-change confirmed: theme changed from light to dark) Same-origin baseline --> 200. Cross-origin probe --> 200. No differential. CSRF protection absent.
Oracle: before/after state read. Benign preference toggle - no funds, credentials or records affected. Severity high - any third-party page the authenticated user visits can forge state-changing requests under their session.

03Token bypass patterns

A CSRF token in the HTML form is not protection until the server validates it on every state-changing request, ties it to the current session, and rotates it after use. Four distinct failure modes each require a separate probe because each returns the same 200 for a different reason - a single check misses the other three entirely.

Failure modeProbeConfirming signalRating
Token absent, not validatedSubmit without the token parameter200 OK, state changes without any token presentHigh
Token present but ignoredSubmit with a random 32-char value in the token field200 OK despite a token the server never issuedHigh
Token tied to account, not sessionUse a valid token from a different session of the same account200 OK - token does not rotate per session or per requestMedium
Double-submit cookie bypassSet a csrf cookie on an owned subdomain; match the form field valueServer validates cookie == form value but both are attacker-controlledHigh

Double-submit cookie protection assumes an attacker cannot write cookies for the target domain. If the application has a subdomain that allows cookie writes (a wildcard Domain=.target.example attribute, or a subdomain with XSS), an attacker can set a known csrf cookie value and match it in the form field. The server's check that the two values match passes, but both values came from the attacker. Each of the four bypass modes is probed independently; a finding is raised only for the modes where the state-change confirmation succeeds.

04SameSite is a layer, not a replacement for tokens

SameSite=Lax (the Chrome default since version 80) blocks cross-site subresource requests - fetch, img src, iframe - but allows cookies on top-level navigations: a link click or a form redirect that results in a full-page load. A state-changing endpoint that responds to a GET request is fully exposed to Lax-cookie CSRF because a link click is a top-level navigation and the browser delivers Lax cookies. SameSite policies and CSRF tokens are independent controls. A gap in either is a separate finding, confirmed separately, with its own evidence and severity.

SameSite valueCross-site subresourceTop-level navigationCSRF exposure
StrictCookies not sentCookies not sentLowest - blocks everything cross-site
Lax (Chrome default)Cookies not sentCookies sentGET-based CSRF still reachable via link click
None (must pair with Secure)Cookies sentCookies sentSame exposure as pre-SameSite browsers
No attribute (older browsers)Cookies sentCookies sentFull cross-site exposure if no token protection
4
token bypass modes, each needing its own probe
0
sensitive operations run during the confirming probe
3
facts required: before state, cross-origin response, after state

The fix is defense-in-depth: a synchronizer token validated server-side on every state-changing request, SameSite=Strict or Lax on session cookies, and Content-Type: application/json enforcement on JSON APIs (browsers do not allow cross-origin preflightless requests for non-simple content types, so a strict content-type check adds a third layer). Rating the finding as medium because SameSite=Lax is present overstates the protection Lax actually provides on GET-based or top-level-navigation paths - the confirmed state-change is the evidence that decides severity, not the presence of a SameSite attribute in the cookie.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →