Authentication · advanced
CSRF validation fails in the browser but not a script: compare cookies and origin
Explains why CSRF validation succeeds for scripted clients but fails for real browsers, framed as a divergence in what request evidence (cookies, Origin/Referer, headers) each client presents. The guide treats the browser as a boundary that silently edits cookies and origin metadata, and gives an evidence-first triage to identify which boundary actually caused the rejection.
The symptoms
- •Server returns 403 Forbidden with a generic CSRF / "invalid token" / "origin not allowed" message for an HTML form or fetch from a logged-in browser tab, while the same payload sent by curl or a test script returns 200.
- •CSRF failure appears only after a successful login redirect, only on the production hostname, or only when the user navigates cross-site (e.g., link from email or another origin) and disappears in incognito.
- •Browser DevTools shows the form/fetch request with a Cookie header but no Origin or Referer, or shows Set-Cookie arriving without the expected SameSite / Secure / Domain / Path attributes the application relies on.
- •The same browser user can load protected pages (cookie is present) but every state-changing endpoint rejects the request with the same CSRF error code, while automation that copies the cookie value still succeeds.
Likely causes
- •Cookie attribute mismatch: the session cookie is marked SameSite=Lax or Strict, or Secure on an HTTP origin, so the browser suppresses it on cross-site navigations while a script (which is same-origin or copies the cookie manually) still sends it.
- •Cookie scoping mismatch: Domain or Path on Set-Cookie excludes the URL the form posts to, or a parent-domain cookie is being shadowed by a more specific host-only cookie issued by a subdomain.
- •Origin/Referer suppression: the browser strips or rewrites Origin (and sometimes Referer) for same-origin requests, certain redirects, sandboxed iframes, or requests initiated from non-HTTP document contexts, causing the server's origin comparison to miss.
- •Reverse proxy or CDN rewriting: a proxy strips or rewrites Origin, Referer, Host, or X-Forwarded-Proto before the application sees the request, so server-side origin checks compare against the wrong host or scheme.
- •Token binding drift: the CSRF token is bound to a cookie value, session ID, or origin string that differs between the browser flow (e.g., after login rotation, after a 308 redirect, after stripping the www. prefix) and the scripted flow.
First ten minutes
- 01Reproduce in the real browser, not in a script: log in, then submit the failing endpoint from the same tab and capture the network request with browser DevTools (Network tab, "Preserve log" enabled) so all redirects and the final POST are retained.
- 02Capture the scripted baseline: from a separate terminal, replay the same payload with the same cookie value and compare response status and body to the browser attempt, without exposing secrets in the command line.
- 03Diff the two requests at the header level (Cookie, Origin, Referer, Host, Sec-Fetch-Site, Sec-Fetch-Mode, Content-Type) and record which fields are present in one client and missing or different in the other.
- 04Inspect the Set-Cookie attributes on the login response (SameSite, Secure, HttpOnly, Domain, Path, Priority/Partitioned) and confirm each attribute matches what the CSRF check expects for the request URL's scheme and host.
- 05Check whether a reverse proxy, CDN, WAF, or ingress controller sits in front of the application and whether it rewrites Host, X-Forwarded-Proto, or strips Origin/Referer headers.
- 06Form a hypothesis tied to the missing field (cookie missing vs. origin missing vs. token mismatch) before changing any application code or policy.
Evidence to collect
- •Browser DevTools Network trace of the failing POST, including the login response Set-Cookie and every redirect, with request and response headers preserved.
- •Server-side access log entry for the failing POST that shows the origin/host/cookie metadata the application actually received, paired with a matching success entry from the scripted replay.
- •Application log line for the CSRF rejection, including the specific check that failed (token mismatch, origin not in allowlist, referer missing, session-bound token absent) and the expected vs. observed values.
- •Effective cookie jar for the failing origin as the browser sees it (DevTools Application tab, including Domain, Path, Expires, SameSite, Secure, HttpOnly, Size, Partitioned columns).
- •Proxy/CDN/WAF configuration snippet that documents header rewriting rules for Host, X-Forwarded-Proto, Origin, and Referer on the affected route.
Where to look
- •Browser boundary: DevTools Network and Application tabs on the failing origin, focusing on the cookies actually attached to the state-changing request and the Sec-Fetch-* hints the browser added.
- •Server boundary: the CSRF middleware or filter in the application, and the access log lines that record the rejection, to confirm whether rejection is "no token", "wrong token", or "origin not allowed".
- •Edge boundary: reverse proxy, load balancer, CDN, or WAF in front of the application, where Host, X-Forwarded-Proto, Origin, and Referer can be silently rewritten or stripped before the application sees the request.
- •Cookie lifecycle boundary: the login response Set-Cookie attributes (SameSite, Secure, Domain, Path, Partitioned) and any subsequent redirect that could re-issue the cookie under a different scope.
- •Token binding boundary: the session store or token cache entry that ties the CSRF token to a session ID, cookie value, or origin string, to detect drift caused by login rotation or post-login redirects.
Diagnostic steps
- 01Compare the browser request headers against the scripted request headers field by field; the first observed divergence (missing cookie, missing Origin, rewritten Host) is the primary suspect.
- 02Cross-check Set-Cookie attributes on the login response against the URL of the failing POST: a Secure cookie on an HTTP page, a Domain that does not include the request host, or a Path that excludes the endpoint will suppress the cookie in the browser only.
- 03Verify SameSite behavior by inspecting Sec-Fetch-Site on the failing request: cross-site navigations will omit a SameSite=Lax cookie on non-GET requests, and a Strict cookie on any cross-site navigation, which scripts do not experience.
- 04Verify origin enforcement by reproducing the failing POST with the browser DevTools "Edit and Resend" feature and toggling Origin/Referer values; if the server accepts an empty Origin for a same-origin request, the failure is likely cookie-side, not origin-side.
- 05Trace the request through the proxy/CDN/WAF: enable verbose access logging at the edge for one request and confirm whether Origin, Referer, Host, and X-Forwarded-Proto arrive intact at the application; if they are rewritten, the application is checking against the wrong value.
- 06Test token binding by logging in, immediately capturing the CSRF token from the rendered page, and submitting it in the same browser session; failure here indicates the token is bound to a session identifier that changed (login rotation, subdomain swap) rather than to the cookie alone.
- 07Confirm the divergence is not a CORS preflight artifact: a CSRF rejection should occur before CORS, so if preflight fails the underlying issue is CORS, not CSRF, and the triage should pivot.
Common mistakes
- •Concluding "the cookie works" because the protected page loads, without checking whether the cookie is actually attached to the state-changing POST under the same scheme/host/path/SameSite context.
- •Adding Origin or Referer to the allowlist from the scripted replay instead of from the browser flow, which masks the real divergence and locks in a permissive rule.
- •Disabling SameSite or lowering it to None without checking that Secure is also set and that the origin actually serves HTTPS, which can shift the failure rather than remove it.
- •Trusting the cookie value copied from DevTools as proof that the browser will send it; the browser decides per-request based on attributes, not on whether the value exists.
- •Assuming the rejection is CSRF when the underlying cause is CORS preflight failure, or assuming it is CORS when the server is actually performing an origin allowlist check on a simple request.
- •Mutating the token validation logic in production before reproducing in a staging environment with the same proxy/CDN/WAF chain, which prevents isolation of the real boundary.
Safe fixes
- •If the cookie is missing on the state-changing request, correct the Set-Cookie attributes on the login response so the cookie is eligible for the request's scheme, host, path, and SameSite context; verify with a second browser trace before deploying.
- •If the server rejects on origin mismatch because the edge rewrites Host or strips Origin, fix the proxy/CDN/WAF forwarding rules so the application receives the original Origin and Host, and add a regression test that asserts those headers survive the edge.
- •If the server's origin allowlist is stricter than the browser can satisfy (e.g., requires an Origin where the browser would send none), narrow the check to documented cases rather than silently accepting empty values, and add a test that exercises the same-origin request.
- •If the CSRF token is bound to a session value that rotates after login or redirect, align the binding with the value the browser actually sends (cookie value, not server-side session ID alone) and confirm with a trace that the token survives the redirect chain.
- •If the fix must lower SameSite to Lax to unblock legitimate cross-site POSTs, pair the change with a CSRF token check that the existing implementation already performs, and document the security trade-off rather than disabling CSRF protection entirely.
- •Each fix should be staged in a non-production environment that mirrors the production edge chain, and rolled out only after the proof-of-fix check passes against the failing browser scenario.
Prove the fix
- 01Re-run the original browser scenario in the affected browser profile and confirm the state-changing endpoint returns 2xx, with DevTools showing the expected Cookie and Origin/Referer attached to the request.
- 02Re-run the scripted replay against the same backend and confirm it still returns 2xx with the same payload, proving the fix did not regress the script path.
- 03Inspect the application log for the rejection reason on the failing endpoint: the "origin not allowed", "token mismatch", or "session token missing" message that previously appeared should be absent for legitimate same-origin requests.
- 04Capture the Set-Cookie attributes on the login response and the headers on the succeeding POST in a saved trace, and attach both to the change record as the regression baseline.
- 05Add an automated check (browser-driven end-to-end test or edge-header assertion) that fails if Cookie, Origin, Referer, or Host are missing or rewritten on the state-changing route, so future proxy or cookie changes re-trigger the failure instead of silently passing.
Prevention and next steps
- •Document the expected Set-Cookie attributes (SameSite, Secure, Domain, Path, HttpOnly, Partitioned) for every authentication cookie in the codebase, and review them whenever the login flow or deployment topology changes.
- •Maintain a header-passthrough contract for the reverse proxy, CDN, and WAF in front of the application: Origin, Referer, Host, and X-Forwarded-Proto must reach the application unchanged for routes that perform CSRF or origin checks, and deviations must alert rather than silently rewrite.
- •Treat CSRF token binding as part of the login flow: if the session identifier, cookie scope, or origin changes between login and the first state-changing request, the token must be re-issued or the binding relaxed in a documented way.
- •Keep a browser-driven smoke test for the top state-changing endpoints (login, checkout, profile update, password change) so that browser-vs-script divergence is caught by CI rather than by a production report.
- •When changing authentication or edge configuration, compare the new browser trace against the saved baseline before and after the rollout; the same payload returning different status codes between browser and script is the signal to investigate the boundary, not to weaken the check.
Safe commands and checks
Read browser DevTools Network headers (Cookie, Origin, Referer, Host, Sec-Fetch-Site, Sec-Fetch-Mode, Content-Type) for the failing request and the scripted replay; record present vs. absent for each field. Read DevTools Application tab Cookies view for the failing origin and record Domain, Path, Expires, SameSite, Secure, HttpOnly, Size, and Partitioned for the session cookie. Read the login response Set-Cookie header from the same trace and compare its attributes against the URL of the failing POST (scheme, host, path). Read the application access log entry for the failing POST to identify which CSRF check rejected it (no token, wrong token, origin not allowed, referer missing, session token missing). Read the proxy/CDN/WAF configuration for the affected route to confirm whether Origin, Referer, Host, or X-Forwarded-Proto are rewritten or stripped before reaching the application. Use a header-diff tool to compare the browser request headers against the scripted request headers field by field, and treat the first observed divergence as the primary suspect before any code change. After deploying a candidate fix, re-capture the same DevTools Network trace and confirm the previously missing header (Cookie, Origin, or Referer) is now present and the request returns 2xx.