Cross-Site Request Forgery (CSRF): The Complete Practical Guide
Preventing CSRF
How to stop CSRF: session-bound synchronizer tokens checked on every method, SameSite cookies, double-submit pitfalls, Origin checks, and read-only GET.
Published
Every CSRF attack needs the same three things: a state-changing action, ambient cookie authentication, and predictable parameters. Prevention is about removing at least one of those, reliably, on every state-changing request. This page collects the defenses in priority order and the pitfalls that make each one fail. Build the habit on a free live target.
The primary control: a synchronizer token
The core defense is a CSRF token that removes the "predictable parameters" precondition by requiring one value the attacker cannot guess. To actually work it has to be all of:
- Unpredictable, generated from a cryptographic random source, not a counter or a timestamp.
- Bound to the user's session, so a token minted in another session is rejected (the cross-account bypass).
- Verified on every state-changing request, regardless of HTTP method, so a GET or a method override cannot skip the check (the method-scoped bypass).
- Rejected when missing or empty, not merely "validated when present" (the remove-the-token bypass).
Most real CSRF findings are a token that fails one of these, so getting all four right is the bulk of the defense.
The browser baseline: SameSite cookies
Set the session cookie's SameSite attribute explicitly:
Laxas a sensible baseline for most apps. Setting it explicitly also avoids the fresh-cookie grace window that a no-SameSite cookie gets.Strictfor the most sensitive applications (banking-grade), accepting that it breaks inbound links while logged in.
Do not rely on SameSite alone. It still allows top-level GET navigations, and sibling subdomains count as same-site, so it is a strong narrowing of the attack surface, not a complete control. SameSite plus a token is the combination.
Double-submit cookie, with care
The double-submit pattern (send the token as both a cookie and a request field, and check they match) is stateless and attractive, but the naive form is broken: anyone who can set the cookie, for instance via a sibling subdomain, sets both halves to the same value and the check passes. If you use double-submit, sign the token so the server can verify it issued it, and scope the cookie tightly. When in doubt, prefer a synchronizer token bound to server-side session state, which has no settable-cookie pitfall.
Defense in depth: Origin and Referer checks
On sensitive endpoints, check the Origin header (with Referer as a fallback) as an additional layer. Prefer Origin because it is harder to spoof from a page and does not leak the full path. Treat this as defense in depth only, never the sole control, because the header can be absent in some flows and a naive match is bypassable. Fail closed when you do check it: a missing or non-matching Origin should reject, not default to allow.
Re-authenticate for the most sensitive actions
For the highest-value actions, password change, email change, MFA reset, large transfers, require the user to re-enter their password or complete a fresh factor. A forged request cannot supply a password the attacker does not know, so re-authentication defeats CSRF on exactly the actions where it hurts most, and it also raises the bar on a stolen or forged session.
Never let GET mutate state
A state-changing GET is forgeable with an image tag and survives the SameSite Lax default (which permits top-level GET). Make every state-changing action a POST (or PUT/DELETE) that carries the token, and keep GET strictly read-only. This one architectural rule removes a whole class of the easiest CSRF.
The short version
- Session-bound, unpredictable CSRF token, verified on every state-changing request regardless of method, rejected when missing.
SameSite=Laxset explicitly (Strict for the most sensitive apps), as a baseline alongside the token.- Double-submit only if signed and tightly scoped, else prefer a server-side synchronizer token.
Origincheck (Referer as fallback) on sensitive endpoints, defense-in-depth, fail closed.- For APIs, require a custom request header (like
X-Requested-With), which a cross-site form cannot set and which forces a CORS preflight. - Re-authenticate for the most sensitive actions.
- GET never mutates state.
And remember that none of this survives XSS: script in the origin reads the token and forges the request anyway, so fixing XSS is part of a complete CSRF posture.
For the offensive side in one place, see the CSRF cheat sheet, and to prove to yourself that a session-bound token on every method really does close the bug, break an app that is missing one on a free live target.
← Back to Cross-Site Request Forgery (CSRF): The Complete Practical Guide