A CSRF (Cross-Site Request Forgery) token is the server-side defense against an attacker tricking a logged-in user into performing actions they didn't intend. The classic attack: malicious site hosts an <img src="https://your-bank.com/transfer?to=attacker&amount=1000"> -- the user's browser dutifully sends the cookie, the bank thinks the user authorized the transfer.
The defense: every state-changing form on your site includes a hidden input with a random per-session value:
<form method="post" action="/transfer">
<input type="hidden" name="_csrf" value="9d8a4f7c-...">
<input name="to"> <input name="amount">
<button>Transfer</button>
</form>
The server validates the token on POST. An attacker on another origin can't read the token (same-origin policy blocks it), so they can't forge a valid form submission.
Framework conventions for the hidden input name:
- Django:
csrfmiddlewaretoken - Rails:
authenticity_token - Laravel:
_token - ASP.NET:
__RequestVerificationToken - Express + csurf:
_csrf - Generic:
csrf_token,xsrf-token
Modern alternatives:
- SameSite cookies (Strict / Lax) — the browser refuses to send the session cookie on cross-origin POSTs, so the request fails authentication regardless. Modern browsers default to Lax, which covers most CSRF attacks.
- Double-submit cookie — server sends a CSRF value as both a cookie AND a custom header; client JS reads the cookie and echoes it in the header on each request. Requires no per-form state.
- Custom request headers — APIs that require
X-Requested-With: XMLHttpRequest(or any custom header) can't be triggered cross-origin without CORS preflight, which the server controls.
Best practice: belt + suspenders. Use SameSite=Lax (or Strict) AND include CSRF tokens in forms. The token catches the residual cross-origin attacks SameSite doesn't (subdomains, browsers without SameSite default).