Testing

Chapter 10
Testing, Pitfalls & Checklist
Finding CSRF, the broken-defence catalogue, and a deployable hardening checklist

The final chapter turns understanding into practice: how to test for CSRF on your own apps, the broken-defence patterns that look protected but aren't, a copy-pasteable hardening checklist — and the proper, deployable fix for the admin-login token error that started this whole course.

How to Test for CSRF

Testing your own endpoints follows a simple manual procedure (and tools automate it):

  1. Capture a legitimate state-changing request (DevTools → Network, or Burp Suite's proxy).
  2. Identify the auth — is it a cookie (CSRF-relevant) or a header token (not classic CSRF)? (Chapter 9)
  3. Strip the anti-CSRF token (and any custom header) and replay the request with only the cookie. If it still succeeds → vulnerable.
  4. Try a forged page — build an auto-submitting form (Chapter 3) on a different origin, load it while logged in, and see if the action fires.
  5. Test the token's strength — does the server accept an empty token? Another user's token? A token after it should have rotated?
The fastest manual test: "does it work without the token?"
The single most revealing check is step 3 — remove the CSRF token (and custom headers) from a captured request and resend it with just the session cookie. If the server still performs the action, the token isn't actually being validated (a shockingly common bug — the token is rendered but never checked server-side). Tools like Burp Suite (the "CSRF PoC generator") and OWASP ZAP automate generating the forged page and flagging missing/unvalidated tokens.

The Broken-Defence Catalogue

These look protected but aren't — the patterns to hunt for in review:

Broken defenceWhy it failsChapter
Token rendered but never validatedserver embeds it, never checks it on POST5
Token only in a cookieauto-rides forged requests; not the synchronizer pattern5
State change on GETforgeable by a bare <img>; SameSite=Lax still sends on top-level GET2,3,7
Naive double-submitcookie injection from a subdomain plants a known value6
Validation on some routes onlyone unprotected endpoint is enough5
"CORS protects us"CORS governs reads, not whether the request is processed2,4
"We use JWT"JWT in a cookie is auto-sent = full CSRF9
Token accepted when empty/blankmissing-token check treats absent as valid5

The Hardening Checklist

Anti-CSRF tokens on every state-changing request — via a maintained library/framework built-in, validated server-side with constant-time comparison
SameSite=Lax (or Strict) + Secure + HttpOnly on the session cookie, set explicitly
Never change state on GET — destructive actions use POST/PUT/DELETE only
Origin/Referer allowlist check (exact-match) on state-changing routes as a corroborating layer
Re-authentication / MFA for sensitive actions (change email/password, payments, delete)
Prevent XSS — output encoding + CSP; it defeats every CSRF defence if present
Use signed double-submit + __Host- cookie prefix if stateless; don't ship the naive variant
Test it — replay without the token; confirm rejection on every state-changing endpoint
★ Your admin-login "Invalid CSRF token" — the deployable fix
Now the full resolution of the error that motivated the course (diagnosed in Chapter 5 as a stale token, not an attack). The fixes, in order of likelihood:
  • Stop caching the login form. Send Cache-Control: no-store (and no-cache) on the login page so Firefox's bfcache doesn't restore a stale page carrying an out-of-date token. This is the most likely single fix given it's Firefox-specific.
  • Issue the token after session regeneration. If login rotates the session (anti-fixation), render/refresh the CSRF token after the new session exists, so the form's token matches.
  • Prefer a per-session token for the login form (or one that tolerates refresh) rather than aggressive per-request rotation, which is what breaks on back/multi-tab.
  • Graceful handling: on a token mismatch for the login specifically, re-render the login page with a fresh token instead of a hard 403 — so the user never sees the error.
Test across browsers (you'd noted only Firefox so far): Chrome's bfcache behaves differently, which is exactly why you saw it there. None of this is a security weakness — it's the defence firing on a stale token; these fixes make it stop annoying you without weakening anything.
Don't "fix" CSRF errors by weakening the defence
A tempting but dangerous response to a nuisance CSRF error is to disable the check, exempt the endpoint, or loosen SameSite to None. Resist this — you'd be trading a usability annoyance for a real vulnerability. The correct fixes (above) address the token-staleness cause (caching, session lifecycle) without removing the protection. If you ever must exempt an endpoint, it should be one that genuinely changes no state.

Hands-On Exercises

Exercise 1

Write a step-by-step manual test plan to determine whether a state-changing endpoint is CSRF-vulnerable, including the "remove the token and replay" check and a forged-page test. State what result at each step indicates a vulnerability.

📄 View solution
Exercise 2

Given the broken-defence catalogue, audit this app: it validates tokens on POST only, exposes GET /admin/delete?id=, stores its CSRF value only in a cookie, and the team says "CORS protects the API." List every CSRF flaw and the fix for each.

📄 View solution
Exercise 3

Write the complete, ordered fix for the admin-login "Invalid CSRF token" error: the headers to add, the session/token lifecycle change, and the graceful-handling behaviour. Explain why each addresses token staleness rather than weakening protection, and why the bug appeared in Firefox.

📄 View solution

Chapter 10 Quick Reference

  • Test by replay — remove the token/custom header, resend with just the cookie; if it works, the token isn't validated
  • Tools: Burp Suite (CSRF PoC generator), OWASP ZAP; manual forged-page test from another origin
  • Broken defences: token rendered-but-unvalidated · token-only-in-cookie · state-changing GET · naive double-submit · partial coverage · "CORS/JWT protects us" · empty-token accepted
  • Checklist: tokens everywhere (library) · SameSite+Secure+HttpOnly · safe-GET · Origin check · re-auth on sensitive ops · prevent XSS · signed double-submit + __Host- · test by replay
  • Login-error fix: Cache-Control: no-store on the login page · issue token after session regeneration · per-session token · re-render gracefully on mismatch
  • It's a stale token, not an attack — fix the staleness; never disable the check or loosen SameSite to "fix" it
  • Test across browsers — Firefox's bfcache made the stale-token issue visible where others may differ

★ CSRF — Cross-Site Request Forgery Complete — 10 / 10 chapters

From the confused-deputy mechanism and ambient authority, through the attack anatomy, the CSRF/XSS/SSRF distinction, every defence (synchronizer & double-submit tokens, SameSite, Origin checks, re-auth), modern SPA/API/JWT considerations, and a deployable testing checklist. You can now recognize CSRF, defend against it in depth, audit broken defences — and you've fully diagnosed and fixed the admin-login token error that started it all.