CSRF token missing or incorrect: what it means and how to fix it
Your form or API call fails with a CSRF error, but only sometimes, only in production, or only from JavaScript. The server is checking that the request really came from your own page, and something in that chain is missing.
What CSRF protection does
Browsers send cookies automatically, so a page on another site could submit a form to your app and the request would arrive logged in as the victim. That is cross-site request forgery. Frameworks block it by requiring a value that another site cannot read: a random token embedded in your own page. A request without the matching token is refused, usually with 403 (Django, Express csurf/csrf-csrf), 419 Page Expired (Laravel) or 422 / InvalidAuthenticityToken (Rails).
The usual causes
| Symptom | Likely cause |
|---|---|
| Every POST from a plain HTML form fails | Hidden token field missing: {% csrf_token %}, @csrf, form_with / authenticity_token |
Forms work, fetch or axios fails | Token not sent as a header, or the cookie not sent (credentials) |
| Works on localhost, fails in production | HTTPS proxy: app thinks it's on http, or trusted origins not configured |
| Fails after the page was open a while | Session expired, so the token no longer matches (Laravel's 419) |
| Fails only in an iframe or from another subdomain | Session or CSRF cookie blocked by SameSite, or the origin isn't trusted |
| Fails only in one browser or private mode | Cookie rejected: Secure on http, SameSite=None without Secure |
| Fails on the second tab or after login | Token rotated on login; the old page still has the previous token |
Sending the token from JavaScript
// Read the token from a meta tag your template renders
const token = document.querySelector('meta[name="csrf-token"]').content;
await fetch('/api/items', {
method: 'POST',
credentials: 'same-origin', // 'include' if the API is on another origin
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
body: JSON.stringify({ name: 'x' })
});
Header names differ: Django expects X-CSRFToken (from the csrftoken cookie), Laravel accepts X-CSRF-TOKEN or X-XSRF-TOKEN (axios sends the latter automatically from the XSRF-TOKEN cookie), Rails uses X-CSRF-Token from csrf_meta_tags. If the cookie the token is compared against never arrives, the token can't match, so check the cookie first.
Behind a proxy or on HTTPS
- Django 4+ checks
Origin: add your scheme and host toCSRF_TRUSTED_ORIGINS = ["https://app.example.com"], and setSECURE_PROXY_SSL_HEADERwhen TLS ends at a proxy. - Express: set
app.set('trust proxy', 1)so secure cookies are issued behind a proxy. - Rails: “HTTP Origin header didn't match request.base_url” means the proxy isn't forwarding
X-Forwarded-Proto/Host. - Laravel: 419 behind a load balancer usually means
TrustProxiesisn't set, orSESSION_DOMAIN/SESSION_SECURE_COOKIEdoesn't match the real host.
Check the cookie itself
Copy the Set-Cookie response header for the session or CSRF cookie from DevTools and paste it into the Set-Cookie explainer, with the page URL. It tells you if the browser rejected it (for example Secure over http, a wrong Domain) and whether it would be sent on a form POST or fetch from your page.
Don't just turn it off
Exempting a view (@csrf_exempt, $except, skip_forgery_protection) is fine only for endpoints that don't use cookie authentication, such as webhooks verified by a signature or APIs that take a bearer token in the Authorization header. For anything a logged-in browser can call, keep the check. SameSite=Lax cookies close most CSRF holes on their own, but not same-site attacks from a sibling subdomain or GET endpoints that change data.
Related
Why is my cookie not being set? · CORS with credentials · localStorage vs cookies · 401 vs 403
Questions
What does 'CSRF token missing or incorrect' mean?
The server expected a secret token in the form or a header that matches the one in your session or cookie, and the request didn't include it or it didn't match.
Why does Laravel show 419 Page Expired?
It's Laravel's CSRF failure: the form had no @csrf token, the session expired, or the session cookie wasn't stored or sent.
How do I send a CSRF token with fetch?
Read it from a meta tag or cookie and send it in a header such as X-CSRF-Token, and include credentials so the session cookie is sent too.
Why does CSRF verification fail only in production?
Usually HTTPS and a proxy: the app doesn't trust the forwarded scheme or host, the origin isn't in the trusted list, or a Secure cookie isn't being set.
Do APIs with bearer tokens need CSRF protection?
Not if they only accept a token in the Authorization header, since browsers don't attach that automatically. Cookie-authenticated APIs do.
Published .