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.
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.
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.
# 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
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 mode | Probe | Confirming signal | Rating |
|---|---|---|---|
| Token absent, not validated | Submit without the token parameter | 200 OK, state changes without any token present | High |
| Token present but ignored | Submit with a random 32-char value in the token field | 200 OK despite a token the server never issued | High |
| Token tied to account, not session | Use a valid token from a different session of the same account | 200 OK - token does not rotate per session or per request | Medium |
| Double-submit cookie bypass | Set a csrf cookie on an owned subdomain; match the form field value | Server validates cookie == form value but both are attacker-controlled | High |
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.
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 value | Cross-site subresource | Top-level navigation | CSRF exposure |
|---|---|---|---|
| Strict | Cookies not sent | Cookies not sent | Lowest - blocks everything cross-site |
| Lax (Chrome default) | Cookies not sent | Cookies sent | GET-based CSRF still reachable via link click |
| None (must pair with Secure) | Cookies sent | Cookies sent | Same exposure as pre-SameSite browsers |
| No attribute (older browsers) | Cookies sent | Cookies sent | Full cross-site exposure if no token protection |
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.