Double-Submit Cookie & Stateless Tokens
The synchronizer token (Chapter 5) requires the server to store a token per session. That's fine for stateful apps, but adds friction for stateless backends and APIs that don't keep server-side session state. The double-submit cookie pattern achieves CSRF protection without server-side storage — at the cost of some subtle pitfalls this chapter makes explicit.
The Core Trick
The server sends the CSRF token as a cookie, and the client-side JavaScript reads that cookie and copies its value into a request header (or body field) on each state-changing request. The server then checks that the cookie value and the header value match. No server-side token store is needed — the server just compares the two copies the request itself carries.
Why It Stops CSRF
The cookie rides automatically on a forged cross-site request (ambient authority) — but the attacker cannot read it to copy its value into the header, because of the Same-Origin Policy (Chapter 2). So a forged request arrives with the cookie but no matching header, and validation fails. The defence rests entirely on the asymmetry: the cookie is auto-sent, but only same-origin JavaScript can read it to set the header.
Synchronizer vs Double-Submit
| Synchronizer token | Double-submit cookie | |
|---|---|---|
| Server-side storage | required (token per session) | none — stateless |
| Validation | submitted token vs stored token | cookie copy vs header copy |
| Best fit | stateful, server-rendered apps | stateless backends, SPAs/APIs |
| Main weakness | storage cost; per-request fragility | subdomain / cookie-injection issues |
The Big Pitfall: Subdomains Can Write Your Cookie
Double-submit's security assumes the attacker can't control the cookie value. But cookies have a weaker isolation model than the same-origin policy: a sibling or compromised subdomain can set a cookie on the parent domain. If evil.example.com (or an XSS on any subdomain) can write the csrf cookie for .example.com, an attacker can plant a known value — then put that same known value in the header of a forged request, and the two match.
csrf-csrf) implement this signed variant — don't roll the naive version yourself.
The Signed / HMAC Variant
The hardened form binds the token to the user's session so it can't be forged by merely setting a cookie:
Now the server doesn't blindly trust the cookie; it verifies the token is one it issued for this session. This restores the property that an attacker who can only set a cookie (but not read the session secret) can't produce a valid token — closing the cookie-injection hole while staying stateless.
The SPA-Friendly Variant You've Probably Seen
Frameworks like Angular and Django (with the right config) ship a double-submit flow out of the box: the server sets a cookie named XSRF-TOKEN, and the framework's HTTP client automatically reads it and sends it back as an X-XSRF-TOKEN header on every request. If you've used Angular's HttpClient and wondered where CSRF protection came from, this is it — double-submit, automated.
Hands-On Exercises
Implement a basic double-submit cookie check in Express (set a JS-readable token cookie; validate that an X-CSRF-Token header equals the cookie). Then explain precisely why a cross-site forged request fails this check despite the cookie being auto-sent.
Describe the subdomain / cookie-injection attack that defeats naive double-submit. Walk through how an attacker controlling evil.example.com can make a forged request pass the cookie==header check, and explain how the signed/HMAC variant prevents it.
Compare synchronizer and double-submit on storage, statelessness, and main weakness, and recommend which to use for (a) a server-rendered Rails app and (b) a stateless JSON API behind a SPA. Then explain why the CSRF token cookie is intentionally not HttpOnly and when that becomes dangerous.
📄 View solutionChapter 6 Quick Reference
- Double-submit cookie — token sent as a cookie; client copies it into a header; server checks cookie == header. No server storage.
- Stops CSRF because the cookie auto-rides but the attacker can't read it (SOP) to set the matching header (which also needs a preflight)
- Favour a custom header over a form field — adds the CORS preflight barrier on top of the read barrier
- Best for stateless backends, SPAs, and APIs; synchronizer is better for stateful server-rendered apps
- Pitfall: a subdomain / sibling XSS / non-HTTPS attacker can write the cookie → plant a known value → naive cookie==header is forgeable
- Fix: the signed / HMAC variant binds the token to the session, so a planted cookie value the server didn't issue fails validation
- Angular (
XSRF-TOKEN→X-XSRF-TOKEN) and Django ship automated double-submit - The CSRF token cookie is intentionally not HttpOnly (JS must read it); the session cookie still should be — and this is safe only absent XSS
- Next chapter: SameSite cookies — how a browser-level attribute changed the whole CSRF landscape