What CSRF Actually Is
The browser attaches your credentials to any request an attacker can cause. The server sees a valid session and cannot tell who asked for it.
Overview
Cross-site request forgery is a confused-deputy problem. The deputy is the browser.
When you are logged into bank.example, your browser holds a session cookie for that origin. The browser's rule for sending that cookie has historically been simple: if the request goes to bank.example, attach it — regardless of which page caused the request. An attacker who can make your browser issue a request to bank.example therefore gets an authenticated request, for free, without ever seeing your cookie.
The server is not tricked into believing a lie. It receives a genuinely authenticated request from a genuinely authenticated user. What it cannot see is intent — whether the user meant to make it, or whether a page on another origin made it on their behalf.
That is the entire vulnerability. Everything else on this site is a variation of it: which shapes of request an attacker can still cause (Delivery), which credentials still ride along (SameSite Semantics), and what a server can check to recover the missing intent (The Synchronizer Token).
The three preconditions
All three must hold. If any one fails, there is no CSRF finding — and mistaking that is the most common way a report gets rejected.
-
A state-changing action. Reading data is not CSRF; an attacker who can cause a request generally cannot read the response (Same-Origin Policy vs CORS). If the only effect is a read, you are looking for a CORS or a leak issue instead.
-
Ambient credentials. The browser must attach the credential automatically. Session cookies do. HTTP Basic, NTLM/Negotiate, and TLS client certificates do. An
Authorization: Bearerheader does not — the application's own JavaScript adds it, and an attacker's page cannot. An API that authenticates only by bearer token is not CSRF-able, no matter how sensitive the endpoint. -
No unpredictable request parameter. Every parameter the server validates must be something the attacker can guess or supply. A per-session token they cannot read is exactly the parameter that breaks this precondition — which is why it is the primary defence.
In 2026 there is effectively a fourth: the cookie has to actually be sent. Chromium's Lax default means many textbook CSRF shapes no longer deliver a cookie at all. See CSRF in 2026.
The minimal attack
<!-- Hosted on the attacker's origin. The victim only has to open this page
while logged into the target. There is no XSS involved and no cookie
is ever read by the attacker. -->
<form id="poc" action="https://bank.example/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="5000" />
</form>
<script>document.getElementById('poc').submit()</script>What the server sees
POST /transfer HTTP/1.1
Host: bank.example
Cookie: session=eyJ1c2VyIjoidmljdGltIn0
Content-Type: application/x-www-form-urlencoded
Content-Length: 26
Origin: https://attacker.example
Referer: https://attacker.example/
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
to=attacker&amount=5000The tell is in the headers, not the body
Look at that request again. The body is indistinguishable from a legitimate transfer. The session cookie is real. The only evidence that something is wrong lives in three headers the browser added and the attacker cannot forge:
| Header | Legitimate request | Forged request |
|---|---|---|
Origin | https://bank.example | https://attacker.example |
Referer | a page on bank.example | a page on attacker.example |
Sec-Fetch-Site | same-origin | cross-site |
That is the whole basis of header-based defence — see Origin, Referer, and Fetch Metadata. It is also why a curl reproduction proves nothing: you control every header in curl, so a request that succeeds there says only that the endpoint exists, not that a browser would have sent it.
What is not CSRF
These come up constantly in triage and none of them is a CSRF finding:
- Logout CSRF, on its own. Forcing a logout is a nuisance, not a security impact. It becomes relevant only as a step in a chain — see Login CSRF and Session Fixation.
- A missing CSRF token on an endpoint that has no side effect. No state change, no finding.
- An endpoint authenticated only by a bearer token or an API key in a header. The browser never attaches it, so there is nothing to forge.
- "The token was accepted when I replayed it with curl." Of course it was — you sent a valid token. The question is whether an attacker's page could have obtained one.
- A GET endpoint that leaks data cross-site. That is a CORS misconfiguration or a JSONP leak, not CSRF, unless it also changes state.
- Reflected input on a page. That is XSS. Note that XSS defeats every CSRF defence there is — script running on the target's own origin can simply read the token — so a CSRF finding on a page that also has XSS is redundant.
Rating the impact
CSRF has no impact of its own; it inherits the impact of the action it triggers. Rate the action, then apply the constraints that are genuinely part of the attack:
- User interaction is required (
UI:Rin CVSS) — the victim must visit an attacker-controlled page while authenticated. This is a real limitation and it belongs in the vector. - The attacker cannot read the response. Anything requiring the response body — reading a balance, exfiltrating a token — is out of scope unless something else supplies it (CORS-Assisted CSRF).
- The browser matters. In 2026 the same PoC can work in Firefox and fail in Chrome. A finding that does not name the affected browsers is incomplete, and a triager who tests in the wrong one will close it.
The highest-value targets are the ones that hand over durable control rather than performing a one-off action: change the account email, add a recovery phone, add an SSH key or an API token, add a second admin, or link an attacker-controlled identity provider (Login CSRF).
Related
What still works and what died with Lax-by-default — the honest, browser-by-browser exploitability verdict, plus the techniques that are now purely historical.
Cookies are scoped by domain, not origin: who can set one, who receives one, and what __Host-, Secure, Path, and Partitioned actually change.
Exactly when Lax, Strict, None, and an absent attribute attach a cookie to a cross-site request — and why the three engines disagree.
CORS gates reading, not sending. Simple versus preflighted requests, opaque responses, and why write-without-read is enough for CSRF.
The urlencoded and multipart form: the only primitive that issues a cross-site request with a body, ambient credentials, and no preflight.
A server-side token bound to the session, plus the lifecycle questions: per-request versus per-session, rotation, BREACH masking, and back-button breakage.
Enumerate, classify by auth mechanism, read the SameSite value first, then test — and write the finding with the browsers named.