The Synchronizer Token Pattern
Now the defences. The synchronizer token pattern is the oldest and most robust CSRF defence, and the one behind the "Invalid CSRF token" message you hit on your admin login. It works by attacking precondition #2 from Chapter 1 — forgeable parameters — by requiring every state-changing request to carry a secret the attacker cannot know.
The Core Idea
The server generates a random, unpredictable token, ties it to the user's session, and embeds it in every form it serves. A genuine request (from the server's own page) includes the token; a forged request (from evil.com) cannot, because the attacker can't read the token out of your page (Same-Origin Policy, Chapter 2). The server rejects any state-changing request whose token is missing or wrong.
The Lifecycle: Generate → Embed → Submit → Validate
What It Looks Like in Code (Express)
The token must be sent in the request body or a header — never only in a cookie, because a cookie auto-rides on forged requests too (that's the whole problem). The defining property is that the token travels somewhere the attacker would have to read or write, which cross-site they cannot.
Per-Session vs Per-Request Tokens
| Per-session token | Per-request token | |
|---|---|---|
| Lifetime | one token for the whole session | new token after every request |
| Pros | simple; tolerant of back button & multiple tabs | tighter; limits token-replay window |
| Cons | longer-lived secret | fragile — back button & multiple tabs cause mismatches |
This table is the crux of your login mystery. Per-request rotation is more secure but notoriously breaks the back button and multi-tab usage, because an old page holds a token the server has already rotated away from.
Generating a Good Token
The token's security rests entirely on being unguessable. Use a cryptographically secure random generator, not Math.random():
Compare tokens with a constant-time comparison (e.g. crypto.timingSafeEqual) to avoid leaking information through timing. And validate on every state-changing request — a single unprotected POST endpoint is all an attacker needs.
Hands-On Exercises
Implement a minimal synchronizer-token defence in Express: generate a token at session start, embed it as a hidden _csrf field, and validate it on a POST route with a constant-time comparison. Explain why the token must be in the body/header and not only in a cookie.
Explain, step by step, why your admin-login "Invalid CSRF token" error occurs and why the browser's Back button clears it. Identify the three likely root causes (stale cached form, session regeneration on login, per-request rotation) and why Firefox's bfcache makes it more visible.
📄 View solutionFor each flawed implementation, explain how an attacker defeats it: (a) the token is stored only in a second cookie and the server checks the two cookies match; (b) only POST routes validate the token while GET /delete?id= exists; (c) the token is generated with Math.random(). State the fix for each.
Chapter 5 Quick Reference
- Synchronizer token — server issues a random token tied to the session; every state-changing request must echo it back
- Works by attacking forgeable parameters — the attacker has the cookie but can't read the token (cross-site)
- Lifecycle: generate → embed (hidden field) → submit (body/header) → validate against the session's stored token
- Token must travel in the body or a header, never only a cookie (a cookie auto-rides forged requests)
- Per-session (simple, back-button friendly) vs per-request (tighter, but breaks back button & multi-tab)
- Your login error = a stale token (cached/bfcached form, session regenerated on login, or per-request rotation); Back reloads a matching form
- Generate with a CSPRNG (
crypto.randomBytes), compare in constant time, validate on every state-changing route - Defeated by: unguarded GETs · token-only-in-cookie · weak token · uncovered endpoints · XSS (reads the token)
- Next chapter: double-submit cookie & stateless tokens — defending without server-side token storage