CSRF in Modern Apps
Classic CSRF assumed server-rendered pages and cookie sessions. Modern apps — SPAs talking to JSON APIs, mobile clients, token-based auth — change the picture significantly. The single most important question for any endpoint is: how does it authenticate the request? Because that answer determines whether CSRF even applies.
The Decisive Question: Cookies or Not?
CSRF exists only because credentials are sent automatically (ambient authority, Chapter 1). So the dividing line is whether your auth credential is auto-attached by the browser:
| Auth method | Credential sent automatically? | CSRF-vulnerable? |
|---|---|---|
| Cookie session | Yes — browser attaches it | YES — needs CSRF defences |
| Authorization: Bearer <token> | No — JS must add it explicitly | No — not classic CSRF |
Authorization: Bearer header that your JavaScript must explicitly attach to each request, then a cross-site attacker's forged request won't carry it — the browser doesn't auto-attach Authorization headers, and the attacker can't read the token to add it (Chapter 2). So a purely header-token API is not vulnerable to classic CSRF. The catch is in the word purely: the moment a token lives in a cookie, or you use any cookie-based session, CSRF is back on the table.
Where Teams Get This Wrong: JWT in a Cookie
A very common pattern is "we use JWTs, so we're stateless and CSRF-safe." But where the JWT is stored decides everything:
| JWT stored in… | Auto-sent? | CSRF risk | XSS risk to token |
|---|---|---|---|
| Authorization header (from JS memory) | No | No classic CSRF | readable by XSS |
| Cookie | Yes | CSRF-vulnerable (needs defences) | HttpOnly hides from XSS |
HttpOnly and survives reloads), the browser auto-sends it on cross-site requests exactly like a session cookie — so you have full classic CSRF exposure and must add tokens/SameSite. If you put it in an Authorization header, you avoid classic CSRF but the token is reachable by XSS. There's a genuine trade-off (CSRF exposure vs XSS exposure), but there is no "JWT = safe." Decide based on storage, not format. This is the most common modern misconception about CSRF.
The "JSON-Only / Custom Header" API Defence
Many JSON APIs lean on the Chapter 2 fact that a forged HTML form can't set headers and can't send application/json without a preflight. So an API that requires Content-Type: application/json and rejects everything else (or requires a custom header) blocks classic form-based CSRF:
application/x-www-form-urlencoded reopens the hole, because a form can send that), and it assumes well-behaved CORS. If the endpoint uses cookie auth, do not rely on JSON-only as the sole CSRF defence — pair it with SameSite + tokens. With header-bearer-token auth it's moot (no CSRF anyway). The point: "we only accept JSON" is a useful layer, not a guarantee.
SPA + Cookie Session: The Double-Submit Sweet Spot
A very common modern setup is an SPA that still uses a cookie session (often the simplest, most secure option since the session cookie can be HttpOnly). Here the double-submit cookie (Chapter 6) fits perfectly: the SPA's HTTP client reads the XSRF-TOKEN cookie and echoes it in an X-XSRF-TOKEN header automatically. This is exactly what Angular's HttpClient and Axios (with config) do out of the box.
Framework Built-Ins — Don't Roll Your Own
| Framework | Built-in CSRF protection |
|---|---|
| Django | CsrfViewMiddleware + {% csrf_token %} (synchronizer/double-submit hybrid); on by default |
| Rails | protect_from_forgery + form_authenticity_token; on by default |
| Spring Security | CSRF tokens enabled by default for browser clients |
| Express | no built-in; use a maintained lib (csrf-csrf; the old csurf is deprecated) |
| Angular | client-side XSRF-TOKEN → X-XSRF-TOKEN double-submit, automatic |
csurf package is a cautionary tale — prefer actively maintained options like csrf-csrf.
Decision Guide
- Cookie/session auth (SSR or SPA) → SameSite + anti-CSRF tokens (synchronizer for SSR, double-submit for SPA). CSRF fully applies.
- Header bearer token (no cookies) → no classic CSRF; focus on XSS (token theft) and token handling instead.
- JWT in a cookie → treat exactly like a session cookie: full CSRF defences required.
- Mobile / native client (no browser) → no CSRF (no ambient cookie behaviour); standard token auth.
Hands-On Exercises
For each API, state whether classic CSRF applies and why: (a) auth via Authorization: Bearer header from JS memory; (b) auth via a session cookie; (c) auth via a JWT stored in a cookie; (d) a native mobile app sending a token header. Tie each answer to "is the credential auto-sent by the browser?"
Explain why "we use JWTs so we're CSRF-safe" is wrong. Contrast JWT-in-header vs JWT-in-cookie on both CSRF exposure and XSS exposure, and state the trade-off a team is actually making when they choose cookie storage for HttpOnly.
📄 View solutionAn API blocks CSRF by requiring Content-Type: application/json. Describe two ways this can still fail (a permissive parser that also accepts form encoding; cookie auth with lax enforcement), and give the correct layered configuration for a cookie-authenticated SPA.
Chapter 9 Quick Reference
- The decisive question: is the auth credential auto-sent by the browser? Cookie = yes (CSRF applies); header bearer token = no
- Authorization: Bearer (added by JS) → not classic CSRF; the attacker can't auto-send or read it
- "We use JWT" ≠ safe — JWT in a cookie is auto-sent → full CSRF exposure; JWT in a header avoids CSRF but is XSS-readable
- Trade-off: cookie storage (HttpOnly, CSRF-exposed) vs header storage (XSS-exposed, no CSRF) — choose by storage, not format
- JSON-only / custom-header API defence works only with strict rejection of other content types; not sole defence under cookie auth
- SPA + cookie session → double-submit (Angular/Axios
XSRF-TOKEN→X-XSRF-TOKEN) is the sweet spot - Use framework built-ins (Django/Rails/Spring on by default; Express use
csrf-csrf, not deprecatedcsurf) — don't hand-roll - Mobile/native clients: no browser ambient cookies → no CSRF
- Next chapter: testing, pitfalls & a hardening checklist — finding CSRF and the deployable summary (incl. your login-error fix)