Authentication · advanced
CSRF validation checklist
A field-tested CSRF validation checklist for engineers debugging state-changing requests that fail token or origin checks. Walks through observable symptoms, the boundary between server-side validation and credential transport, diagnostic steps that separate SameSite, double-submit, and synchronizer-token failures, and proof-of-fix regression criteria grounded in HTTP cookie semantics.
The symptoms
- •POST, PUT, PATCH, or DELETE requests return 403 Forbidden with a body referencing "CSRF", "Invalid token", "Origin mismatch", or "_csrf" — even when the request originates from the in-app frontend.
- •State-changing endpoints succeed when invoked via server-side scripts or curl with a manually copied session cookie, but fail from the browser — indicating cookie attachment but token absence.
- •Failure rate spikes after a deployment, framework upgrade, or reverse-proxy configuration change, especially when the proxy terminates TLS or rewrites origins.
- •Requests from third-party origins are rejected at the framework middleware layer before the route handler executes, while GET requests against the same session succeed.
- •Intermittent 403s correlate with users navigating from external links (link-based login, password reset email) where the Referer header is absent or cross-origin.
Likely causes
- •Missing or stripped CSRF token on the request: the framework expects a token in a header (e.g., X-CSRF-Token, X-XSRF-TOKEN) or hidden form field, but the frontend is not reading and echoing it back.
- •Cookie attribute mismatch: the session cookie is issued without SameSite, or with SameSite=None without Secure, causing browsers to omit it on cross-site requests — which then triggers the token check against a non-existent session.
- •Origin or Referer header validation configured with an allowlist that excludes the deployed scheme, host, or port — or that compares case-sensitively against a registered origin.
- •Reverse proxy, CDN, or load balancer rewrites or strips the Origin or Referer header before the request reaches the application, breaking server-side origin validation.
- •Token store desynchronization: the token is tied to an in-memory session (e.g., express-session without a shared store) and is lost when the user is routed to a different application instance behind a load balancer.
- •Double-submit cookie pattern broken by a framework that rewrites the cookie name, path, or domain — causing the client-side stored value to diverge from the cookie value the server reads.
First ten minutes
- 01Reproduce the failure with a single state-changing request from the affected browser, recording the exact request method, URL, headers (Origin, Referer, Cookie), and response status + body.
- 02Compare the failing request's headers against a known-good request from the same session; flag any header present in the good request that is absent or altered in the failing one.
- 03Inspect the Set-Cookie response header on the login or session-establishing response and note the Secure, HttpOnly, SameSite, Path, and Domain attributes — these govern whether the browser will attach the cookie on the subsequent request.
- 04Check whether the application is fronted by a reverse proxy or CDN that might rewrite headers; the configuration of that proxy is the next boundary to inspect, not the application itself.
- 05Identify the CSRF strategy in use (synchronizer token, double-submit cookie, origin-only check, custom header) by reading the framework middleware order — the strategy determines which header or cookie the failure points to.
- 06Determine whether the failure is deterministic (every request fails) or session-scoped (only some users, only after a redirect) — the scope narrows the cause between token generation and token transport.
Evidence to collect
- •Full request and response headers for a failing state-changing request, with the request's Origin, Referer, Cookie, and any custom token header (e.g., X-CSRF-Token) recorded verbatim.
- •The Set-Cookie header from the session-establishing response, with each attribute (SameSite, Secure, HttpOnly, Path, Domain, Expires) called out separately.
- •Framework CSRF middleware configuration: which routes are protected, which methods are exempt, and how the token is generated, stored, and verified.
- •Reverse proxy or load balancer header-passing rules, specifically whether Origin and Referer are forwarded, rewritten, or stripped.
- •Application logs at the moment of the 403, including any middleware-emitted reason string (e.g., "invalid CSRF token", "origin not allowed").
- •For clustered deployments: whether the session store is shared (Redis, database) or per-instance (in-memory), and the load-balancing stickiness policy.
Where to look
- •The browser DevTools Network tab for the failing state-changing request — the boundary between client emission and server reception.
- •The framework's CSRF middleware source or configuration file (e.g., Django's CSRF middleware settings, Express csurf/csurf-cookie middleware, Spring Security's CsrfTokenRepository, Rails' protect_from_forgery).
- •The reverse proxy configuration file (nginx config, Apache vhost, CDN edge rules) at the boundary where the proxy terminates the client connection and forwards to the origin.
- •The session store configuration — the boundary where session data, including the CSRF token reference, is persisted and retrieved.
- •Authentication bootstrap: the login handler and any post-login redirect handler, where the session cookie is first set and the token is first issued.
- •The application access log filtered to 403 responses on state-changing methods — the boundary at which validation rejects the request.
Diagnostic steps
- 01Verify the token is round-tripping: in the failing request, confirm the CSRF token header or field value matches the value the server issued for that session. Mismatch means the frontend is not reading the token correctly or the session rotated.
- 02Verify the session cookie is actually attached: the Cookie header on the failing request must contain the session identifier. If absent, the failure is upstream of CSRF — it is a cookie-attribute or SameSite issue, not a token issue.
- 03Verify the Origin header matches the configured allowlist: if the framework checks Origin, the allowlist must include the exact scheme + host (+ port if non-default). A mismatch indicates a deployment hostname or proxy rewrite problem.
- 04Verify the Referer header is present and matches the expected origin if Referer is used instead of Origin; some browsers omit Referer for privacy reasons, so a missing Referer can break Referer-only checks.
- 05Verify the proxy is not stripping headers: send a test request and inspect the headers the application actually receives, not the headers the browser sent. Stripping at the proxy boundary is invisible to the browser.
- 06Verify the token store is consistent across instances: if behind a load balancer with round-robin, issue a token on instance A and verify against instance B. Per-instance token stores cause session-scoped failures.
- 07Verify the exempt-method list: GET, HEAD, and OPTIONS are conventionally exempt from CSRF; if a state-changing endpoint is mistakenly registered as safe, the failure is a design error, not a transport error.
Common mistakes
- •Assuming a 403 is authentication failure when it is actually CSRF validation; both can return 403, but CSRF middleware usually emits a distinct reason string worth searching for in logs.
- •Adding the CSRF token to the cookie instead of reading it from a separate source; the double-submit pattern requires the client to read the cookie value and echo it in a header, not rely on the server to compare cookie to cookie.
- •Configuring SameSite=Lax or SameSite=Strict when the application relies on cross-site flows (embedded forms, third-party redirects) without testing those flows first; the cookie will not be sent, and the token check will fail against an unauthenticated session.
- •Trusting the Origin header alone without HTTPS — Origin can be spoofed on plaintext connections, so origin-only validation is only meaningful when the transport is authenticated.
- •Forgetting that the framework's CSRF protection expects the token to be read on the GET request that renders the form and echoed on the subsequent POST; a missing token in the form template will surface as a validation failure on submit, not on form render.
- •Deploying new SameSite=None cookies without the Secure attribute — modern browsers will reject the cookie entirely, which looks like a CSRF failure but is actually a cookie-rejection failure.
Safe fixes
- •If the token is missing from the request: update the frontend to read the CSRF token from the cookie or meta tag issued on the prior response and echo it in the required header (e.g., X-CSRF-Token) on every state-changing request. Confirm via DevTools that the header is now present.
- •If the session cookie is not attached: adjust the cookie's SameSite and Secure attributes to match the request's cross-site context. For cross-site flows, SameSite=None; Secure is required; for same-site flows, Lax is sufficient. Verify the new attribute is reflected in the Set-Cookie response header.
- •If the origin allowlist is wrong: update the allowed origins list to include the deployed scheme, host, and non-default port exactly as the browser sends them — do not include trailing slashes or paths. Confirm by issuing a request with a matching Origin and observing a 2xx response.
- •If the proxy is stripping headers: update the proxy configuration to forward Origin and Referer unchanged to the origin application. Confirm by comparing the headers the application logs receive against the headers the browser sent.
- •If the token store is per-instance: move session and CSRF token storage to a shared backend (Redis, database, or sticky sessions) so that any instance can validate a token issued by any other. Confirm by round-tripping a token across two instances.
- •If the framework's exempt list is wrong: add the safe methods (GET, HEAD, OPTIONS) to the CSRF-exempt list and remove any state-changing endpoints that were mistakenly exempted. Confirm by submitting a state-changing request to a previously-exempt endpoint and observing a 403 instead of a 200.
Prove the fix
- 01Submit a state-changing request from the in-app frontend with the corrected headers and cookies; observe a 2xx response and the intended state change (e.g., a new record visible in the database, a status field updated).
- 02Submit a cross-site state-changing request (e.g., from a different origin or via a simulated form post) and observe a 403 or equivalent rejection, confirming that the CSRF protection still blocks unauthorized cross-site requests.
- 03Confirm via the Set-Cookie response header on the session-establishing response that the cookie's SameSite, Secure, and Path attributes match the deployed topology — this is the durable guarantee that browsers will continue to attach the cookie.
- 04Run a regression check across multiple application instances behind the load balancer: a token issued on instance A must validate on instance B, and vice versa.
- 05Inspect the application access log over a representative window and confirm that 403s on state-changing methods are no longer correlated with the previously failing user cohort or referrer pattern.
Prevention and next steps
- •Document the CSRF strategy in use (synchronizer token, double-submit, or origin check) alongside the session configuration, so that any change to one is reviewed against the other.
- •Add an integration test that issues a state-changing request from the same origin and asserts a 2xx, plus a cross-origin request that asserts a 403 — these two tests catch the majority of CSRF regressions.
- •When changing reverse proxy or CDN configuration, verify that Origin, Referer, and Cookie headers are forwarded unchanged; header stripping at the edge is the most common silent CSRF failure.
- •Pin the CSRF middleware version explicitly and review its changelog on upgrades; framework upgrades frequently change default token names, cookie attributes, or exempt lists.
- •Use a shared session store in clustered deployments from the start; per-instance stores create token-validation failures that only appear under load balancing.
Safe commands and checks
grep -nE "csrf|CSRF|xsrf|XSRF" <path-to-framework-config> # locate the CSRF middleware configuration in the application source. grep -nE "Origin|Referer|proxy_set_header" <path-to-nginx-or-apache-config> # verify the reverse proxy forwards the headers the CSRF check depends on. grep -E "403|CSRF|invalid token|origin" <path-to-application-access-log> | tail -n 100 # surface recent CSRF rejections with their reason strings. grep -nE "SameSite|Secure|HttpOnly" <path-to-session-or-cookie-config> # confirm the cookie attributes match the deployment topology. grep -nE "protect_from_forgery|csrf_exempt|csrf_token|csrfmiddlewaretoken" <path-to-app-source> # enumerate routes and middleware that participate in CSRF validation.