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:

  • Lax as a sensible baseline for most apps. Setting it explicitly also avoids the fresh-cookie grace window that a no-SameSite cookie gets.
  • Strict for 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.

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=Lax set 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.
  • Origin check (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