Authentication · advanced

How to test CSRF token rotation without invalidating active forms unexpectedly

CSRF token rotation is meant to refresh the anti-forgery credential without abandoning tokens still bound to in-flight form submissions. The bug to verify is the opposite: rotation that silently invalidates active forms, or rotation that preserves stale tokens beyond their intended window. This guide frames rotation as a state-transition contract — pre-rotation tokens, the rotation event, and post-rotation tokens — and gives engineers a way to test the boundaries of that contract from observable HTTP evidence alone. The contract has three observable parts: the Set-Cookie attributes that carry the new token, the server-side acceptance window applied to submitted tokens, and the synchronization rules that decide which form-render moment triggers rotation.

The symptoms

  • Forms previously rendered in a long-lived tab suddenly fail with a generic "session expired" or 400 response on submit, even though the user never logged out or idled past the session timeout.
  • The application issues a Set-Cookie that changes the CSRF token value mid-session, but the new token is not accepted on the next form submit until a hard reload; in-flight requests submitted before the reload are rejected.
  • Conversely, tokens issued before a rotation event remain accepted long after the rotation boundary, allowing a token captured before login or before privilege change to be replayed successfully.
  • Tests that pass against a single-request fixture fail under realistic multi-tab or back-button navigation, because the server's acceptance window does not align with the client-render rotation trigger.
  • The CSRF rejection rate in logs spikes only on a specific transition path — after a soft re-auth, after a long-lived form page is submitted, or after a token refresh — not on first-load submissions.

Likely causes

  • The rotation trigger is bound to a render event (page navigation, form open) rather than to a server-side token issuance, so tokens already in the DOM of open tabs are orphaned when the next rotation fires.
  • The server invalidates the previous token value on every successful issue of a new one, with no grace window; any form rendered before the rotation loses its bound token immediately.
  • Set-Cookie attributes for the rotated token disagree with the form-submission cookie scope (Path, SameSite, Secure), so the browser keeps the old token bound to one path while the server has rotated to a new one.
  • The application treats a CSRF token refresh as if it were a session identifier refresh: it re-issues on every request, which couples token lifetime to request frequency and produces unpredictable invalidation.
  • Long-lived forms (multi-step checkout, document editors, file uploads) carry the token at first render and never re-read it, so a rotation that fires after render is invisible to the form until submit.
  • Token storage and form rendering use different sources of truth — for example, the cookie store versus a meta tag versus a hidden input hydrated from server state — and only one of them is rotated.

First ten minutes

  1. 01Capture one reproduction: open a session, render a form that submits a CSRF token, then trigger the documented rotation event (re-auth, privilege change, timeout-based refresh) without closing the form tab, and submit the original form. Record the response status and body verbatim.
  2. 02Inspect the Set-Cookie header on the rotation response and compare the cookie name, value, Path, SameSite, Secure, and Expires/Max-Age attributes against the pre-rotation cookie, using the fields documented in the MDN Set-Cookie reference.
  3. 03Read the application's CSRF rejection log or audit event for the failed submit; identify whether the rejection cites "token unknown," "token expired," "token replayed," or "token bound to different session," because each implies a different rotation contract.
  4. 04Verify whether the rejected form was rendered before or after the rotation by checking the form's HTML source timestamp or the response that originally delivered it; this distinguishes orphaning (token invalidated while form was open) from stale-submit (form fetched a stale token).
  5. 05Decide whether the symptom is invalidation of active forms (rejection of in-flight tokens) or acceptance of stale tokens (tokens surviving past their window); the two require opposite fix directions and must not be conflated.

Evidence to collect

  • The pre-rotation Set-Cookie header line for the CSRF cookie: name, value prefix (do not record full value), Path, SameSite, Secure, Expires or Max-Age.
  • The rotation response Set-Cookie header line for the new CSRF cookie, with the same attribute set, plus any companion session cookie change in the same response.
  • The HTTP status and response body of the form submit that crossed the rotation boundary; specifically whether the server returned 400, 403, 419, or a 200 with an HTML re-login marker.
  • The server-side rejection reason field for that submit (token_unknown, token_expired, token_replayed, session_mismatch, or vendor-specific equivalent) and the timestamp relative to the rotation event.
  • The list of open form pages and their last-rendered timestamp, to determine which forms were "in flight" at the moment rotation fired.

Where to look

  • The HTTP boundary: the response headers of the rotation-triggering request and the subsequent form-submit request; rotation is observable only at this layer.
  • The session store boundary: the record that maps issued token values to session identifiers and issuance timestamps; the rotation contract is enforced here, not in the form HTML.
  • The form-render boundary: the server-side template or hydration call that emits the CSRF token into the hidden input or meta tag; mismatches between this source and the cookie are the most common silent-break cause.
  • The cookie-application boundary: browser cookie jar entries visible via developer tools, comparing Path, SameSite, and Secure between the old and new CSRF cookies to detect scope mismatch.
  • The audit-log boundary: CSRF rejection events with reason codes, time-bucketed against the rotation event to confirm causation rather than coincidence.

Diagnostic steps

  1. 01Reproduce with controlled timing: render form A, record the token value prefix and the response that delivered it; trigger rotation; submit form A without re-rendering; submit form B freshly rendered after rotation; compare both responses.
  2. 02Compare the server's acceptance rule across the boundary: does the server accept only the latest issued token, any token issued in the current session, or any token issued within a time window? Each rule predicts a different failure shape.
  3. 03Cross-check the Set-Cookie attribute set against the path of the form-submit endpoint; if the new CSRF cookie has a narrower Path than the submission endpoint, the browser will not send it on submit.
  4. 04Verify the SameSite attribute on both cookies: a SameSite=Strict cookie issued by an XHR-style rotation may not be attached to a top-level form submit on a different navigation, producing a phantom rejection that looks like token invalidation.
  5. 05Inspect whether the rotation response also rotates the session cookie; if session and CSRF rotation are coupled, a successful CSRF rejection may actually be a session-binding rejection mislabeled as CSRF.
  6. 06Determine whether the form's hidden CSRF input is read at submit time or frozen at render time; frozen-at-render forms are the canonical victim of mid-session rotation.
  7. 07Test the inverse failure mode: capture a token before a privilege change, complete the privilege change, then attempt to replay the pre-change token; the server's response distinguishes strict-rotation from grace-window rotation.

Common mistakes

  • Conflating "session expired" with "CSRF rejected": a 419 or generic re-login page often means session binding, not token rotation, and patching CSRF rotation will not fix it.
  • Assuming rotation must invalidate all prior tokens: this is the very bug under verification; many production systems deliberately accept prior tokens for a short grace window to avoid orphaning active forms.
  • Reading only the cookie value and ignoring Path, SameSite, and Secure: a correctly rotated token can still fail to attach to the submit request if any of these attributes narrowed.
  • Testing rotation only in a single-tab single-request fixture: this hides the orphaning path because there is no "active form" to orphan.
  • Trusting the absence of CSRF rejections in normal traffic as proof of correct rotation; the bug appears only on the transition, not in steady state.
  • Rotating the CSRF token on every request "to be safe"; this couples token lifetime to request frequency and produces the same orphaning symptom under any latency spike.

Safe fixes

  • Define an explicit acceptance rule and write it down: which token values are valid for how long after issuance, and whether prior tokens remain valid during a grace window. The fix direction depends entirely on this rule being stated, not implied.
  • If active forms are being orphaned: introduce or widen a grace window in which the previously issued token remains acceptable for forms already rendered, and bind the grace window to the form's render timestamp rather than to request count.
  • If stale tokens are being accepted past their window: shorten the grace window and add a session- or privilege-binding check that the server enforces on every CSRF-validated submit, not only at login.
  • Ensure the Set-Cookie attributes of the rotated CSRF cookie match those of the original on Path, SameSite, and Secure, so the browser attaches the new token to the same endpoints as the old one; verify against the MDN Set-Cookie attribute semantics.
  • Decouple CSRF token rotation from session identifier rotation: rotating one should not imply rotating the other, unless the documented contract says otherwise; coupling them is a common source of mislabeled rejections.
  • For long-lived forms, re-read the CSRF token from the cookie store or a meta-refresh endpoint at submit time rather than embedding it once at render time; this is a client-side mitigation only valid if the server still accepts the latest token.

Prove the fix

  1. 01Multi-tab reproduction: open two tabs both rendering the protected form; trigger rotation in tab one; submit the form in tab two without reload; observe a 2xx response and a successful state transition recorded server-side.
  2. 02Stale-replay rejection: capture a token, complete a privilege change that triggers rotation, replay the captured token; observe a rejection whose reason code explicitly distinguishes "pre-rotation token" from "unknown token."
  3. 03Set-Cookie parity check: diff the Path, SameSite, Secure, and Expires/Max-Age of the rotated cookie against the pre-rotation cookie on the same endpoint; all four must be equal in any fix that claims not to change cookie scope.
  4. 04Audit-log signature: the CSRF rejection event rate during a controlled rotation burst drops to zero for forms rendered before rotation, while the rejection reason for genuinely stale tokens remains recorded and time-aligned with the rotation event.
  5. 05Regression boundary: a deliberately slow form (artificial client-side delay between render and submit) crosses the rotation event cleanly and submits successfully, demonstrating the grace window applies to render-time, not request-time.

Prevention and next steps

  • Treat the rotation contract as a documented specification with three named transitions: issuance, grace window, and strict window; each transition must have an observable HTTP signature and a server-side enforcement point.
  • Keep CSRF token rotation independent of session identifier rotation unless the threat model explicitly requires coupling; document any coupling in the authentication specification.
  • Render long-lived forms so that the CSRF token is re-read at submit time from a server-controlled source, not embedded once at render time; this is the structural fix for orphaning and does not depend on grace-window tuning.
  • Add a regression test that opens a form, triggers rotation out-of-band, and submits the original form; a passing test on this path is the only reliable signal that the grace window is wide enough.
  • Include Set-Cookie attribute parity as a CI check on any change that touches CSRF or session middleware; attribute narrowing is the most common silent break.

Safe commands and checks

Use the browser developer tools "Application" panel to export the cookie jar entries for the CSRF and session cookies before and after the rotation event; record name, value prefix, Path, SameSite, Secure, and Expires for diffing. Do not paste values into shared artifacts.
From the captured rotation response, copy the Set-Cookie header lines into a side-by-side comparison with the pre-rotation Set-Cookie; verify Path, SameSite, Secure, and Expires or Max-Age match. Attribute semantics are documented in the MDN Set-Cookie reference.
From the audit log or application log, filter rejection events by reason code in the time window that brackets the rotation event; sort by reason to distinguish orphaning (token_expired, token_unknown) from session-binding rejections (session_mismatch).
For the form-submit request, record the Cookie request header sent by the browser (visible in developer tools network panel) and confirm it contains the rotated CSRF cookie value, not the pre-rotation one; absence here means cookie scope, not rotation logic, is the fault.