# CSRF tokens: when you need them and the setup

## TL;DR
Any request that changes something and rides on cookies needs CSRF protection. Start with SameSite=Lax on session cookies, which kills most CSRF for free, then add synchronizer tokens on the sensitive state-changing endpoints and verify them server-side. GET requests should never change state, which removes a whole class of the problem.

```text
CSRF tokens: when you need them and the setup
```

## Use this when
- You are reviewing forms, account settings, or payment flows
- An audit asks which endpoints have CSRF protection and why
- You are deciding whether a JSON API needs tokens or not
- A framework's CSRF middleware is disabled and someone asks if that is fine
- An agent is mapping state-changing routes to their protections

## Not for this skill when
- The flaw is XSS (different fix, though related)
- You are configuring CORS for cross-origin API access
- Authentication uses bearer tokens in headers rather than cookies
- The question is about clickjacking (see the security headers skill)

## Steps

### 1. List the state-changing routes
Walk the route table and mark every endpoint that creates, updates, or deletes anything. Those are the CSRF surface. Read-only GETs are not, provided they truly change nothing.

```bash
grep -rn "methods=.*POST\|\.post(\|\.put(\|\.delete(\|\.patch(" --include="*.py" --include="*.js" --include="*.ts" [HOME]/... | head -40
```

Expected: a concrete list of mutating endpoints. Anything on this list without a CSRF story is a finding.

### 2. Set SameSite=Lax on session cookies
SameSite=Lax stops the browser from sending cookies on cross-site POSTs, which neutralizes classic CSRF with one cookie attribute. It is the cheapest protection you can deploy.

```bash
curl -sI https://example.com/ | grep -i "set-cookie"
```

Expected: the session cookie carries SameSite=Lax or SameSite=Strict. If it says SameSite=None, there had better be a documented reason and tokens everywhere.

### 3. Add synchronizer tokens where it matters
For the sensitive endpoints (password change, email change, payments, privilege changes), issue a per-session random token, embed it in the form as a hidden field, and verify it matches server-side on submission. The attacker's site cannot read the token, so it cannot forge the request.

Expected: the token is random per session, verified on every mutating request to protected endpoints, and rotated at login. A token that is the same for all users is decoration.

### 4. Verify Origin and Referer as a second layer
For API-style endpoints, checking that the Origin or Referer header matches your own site catches forged requests even if token handling has a bug. Treat it as defense in depth, not the primary control, since some setups strip these headers.

Expected: the check exists on the sensitive endpoints and fails closed (missing header means reject) unless you have documented why a client cannot send one.

### 5. Keep GET requests side-effect free
If a GET changes state (unsubscribe links, "delete" links), it is CSRF-able by a simple image tag on any page. Convert those to POST forms with tokens, or at minimum require a confirmation step that posts.

Expected: a review of GET handlers confirms none of them mutate data. Each converted endpoint gets a test that a bare GET no longer performs the action.

### 6. Test the protection like an attacker
Build a page on a different origin with an auto-submitting form targeting your endpoint, submit it in a logged-in browser, and confirm it is rejected.

Expected: the forged request fails while the legitimate same-origin form succeeds. Keep this as a regression test so a future refactor cannot silently drop the middleware.

### Variant: JSON APIs with cookie auth
A JSON API that relies on cookies needs CSRF protection too; "it is an API" is not immunity. Either add tokens or require a custom header that browsers do not send cross-origin on simple requests, and verify it server-side.

### Variant: APIs with bearer tokens
If every request carries its auth in the Authorization header and cookies are not used for auth at all, classic CSRF does not apply, because the attacker's page cannot produce the header. Confirm no endpoint also accepts cookie auth as a fallback.

### Variant: double-submit cookie pattern
When the server cannot keep per-session token state (stateless setups), set the token in a cookie and require the same value in the request body or header. It works because the attacker cannot set or read the cookie value cross-origin. Prefer the synchronizer pattern when session state is available.

## Why this happens
Browsers attach cookies to requests automatically, which is convenient right up until a malicious page exploits that automation. CSRF is the gap between "the request carries valid credentials" and "the user intended this request". SameSite closed most of it at the browser level, but Lax still allows top-level GETs and older browsers ignore the attribute, so tokens remain the explicit proof of intent.

## Edge cases and pitfalls
- Login CSRF (forcing a victim into the attacker's account) matters on shared machines; protect the login form too.
- Token-per-request schemes break the back button and parallel tabs; per-session tokens are the pragmatic default.
- File upload endpoints are state-changing and often forgotten; include them in the token coverage.
- CORS misconfiguration can re-expose what CSRF tokens protect; review both together.
- Framework CSRF middleware sometimes exempts AJAX by default; verify the exemption matches your actual client behavior.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_K10B9qiz3zOhElXzUjEjgg
