CSRF Token Mismatch: Diagnosing and Fixing 403 Forbidden Errors

Quick answer

💡A CSRF token mismatch means the token submitted with the form or AJAX request does not match the token the server expects for the current session. Check that the token is being read from the correct source (cookie, meta tag, or hidden field), transmitted in the correct request header or body field, and that the session is not being invalidated between the page load and form submission.

Error symptoms

  • HTTP 403 Forbidden with message: CSRF token mismatch or invalid CSRF token
  • InvalidCSRFTokenException: CSRF token does not match stored token
  • Form submissions fail on the first attempt after a long idle period but succeed after page refresh
  • AJAX POST requests return 403 even though GET requests to the same API succeed
  • CSRF validation fails only in certain browsers or after a browser back-navigation
  • Single-page application loses CSRF token state after a hard page refresh

Common causes

  • The session was invalidated or expired between page load (when the token was issued) and form submission
  • The CSRF token is read from a cookie but the client sends it in the wrong request header name
  • Multiple browser tabs with the same session regenerate the CSRF token in one tab, invalidating tokens in other tabs
  • The server regenerates a new CSRF token on every request instead of once per session, causing race conditions
  • A reverse proxy or CDN strips custom headers like X-CSRF-Token before the request reaches the application
  • The application performs a login redirect that generates a new session, invalidating the CSRF token from the pre-login page

When it happens

  • After a user has been idle on a form page for longer than the session timeout
  • When a load balancer routes subsequent requests to a different server that does not share session storage
  • After implementing a security fix that rotates CSRF tokens on privilege escalation (login)
  • When caching middleware caches a page with an embedded CSRF token and serves the stale token to multiple users

Examples and fixes

The synchronizer token pattern issues a per-session token that must be included in every state-changing request.

Synchronizer token pattern in Express

❌ Wrong

// No CSRF protection - vulnerable to forged requests
const express = require('express');
const app = express();

app.post('/transfer', (req, res) => {
  const { amount, toAccount } = req.body;
  processTransfer(amount, toAccount);
  res.json({ success: true });
  // No CSRF check: any site can forge this request
});

✅ Fixed

const express = require('express');
const csrf = require('csrf-csrf').doubleCsrf;
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser());
const { generateToken, doubleCsrfProtection } = csrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: 'csrf-token',
  cookieOptions: { sameSite: 'strict', secure: true }
});

app.get('/transfer', (req, res) => {
  const csrfToken = generateToken(req, res);
  res.render('transfer', { csrfToken });
});

app.post('/transfer', doubleCsrfProtection, (req, res) => {
  processTransfer(req.body.amount, req.body.toAccount);
  res.json({ success: true });
});

The fixed version uses the csrf-csrf package (a maintained replacement for the deprecated csurf) to generate a per-session token on the GET route and verify it on the POST route. The token is embedded in the HTML form as a hidden field or sent as the X-CSRF-Token request header for AJAX. The library validates the submitted token against the server-generated value. If the token is missing or does not match, the middleware throws a ForbiddenError which should be caught and returned as a 403 response. The CSRF_SECRET environment variable must be set to a cryptographically random value unique to the deployment.

The double-submit cookie pattern avoids server-side session storage by requiring the client to send the same value in both a cookie and a request header.

SPA double-submit cookie pattern

❌ Wrong

// Client sends form data without CSRF token in header
fetch('/api/update-profile', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',
  body: JSON.stringify({ name: 'Alice' })
  // No CSRF token: exploitable via cross-site fetch
});

✅ Fixed

// Server sets a readable CSRF cookie on page load
res.cookie('csrf-token', crypto.randomBytes(32).toString('hex'), {
  sameSite: 'strict',
  secure: true
  // Note: NOT httpOnly so JavaScript can read it
});

// Client reads cookie and echoes it as header
function getCsrfToken() {
  return document.cookie
    .split('; ')
    .find(row => row.startsWith('csrf-token='))
    ?.split('=')[1];
}

fetch('/api/update-profile', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCsrfToken() // must match the cookie
  },
  credentials: 'include',
  body: JSON.stringify({ name: 'Alice' })
});

The double-submit cookie pattern sets a random token in a non-httpOnly cookie (JavaScript must read it) and requires the client to echo that exact value in a request header. The server verifies the header matches the cookie. A cross-site attacker cannot read the cookie due to the Same-Origin Policy, so they cannot replicate the header value. This pattern works without server-side session storage, making it suitable for stateless JWT-based APIs. The combination of SameSite=Strict and HTTPS is required for this pattern to be secure.

What causes CSRF token mismatch errors

Cross-Site Request Forgery (CSRF) is an attack where a malicious website causes a victim's browser to submit a request to a target website where the victim is authenticated. The browser automatically includes cookies with cross-site requests, so without CSRF protection, the target server cannot distinguish a request that came from the legitimate application from one that came from an attacker-controlled page.

The synchronizer token pattern, recommended by OWASP CSRF Prevention Cheat Sheet, addresses this by embedding a random secret in both the server session and the page HTML. When the form is submitted, the server compares the submitted token to the session token. An attacker's forged request cannot include the correct token because it was never loaded from the target application's page. The mismatch error you see is the protection working correctly; your task is to ensure legitimate requests include the correct token.

A token mismatch in a legitimate request almost always points to a synchronization problem. The most common cause is session expiry: the user loaded the page, the token was embedded in the HTML, the session expired while the user was filling out the form, and when the user submitted, the server could no longer look up the session to compare the token. The fix is either to extend the session lifetime, use per-session rather than per-request tokens, or implement a graceful re-authentication flow that preserves the form data.

Another frequent cause is multiple tabs. If the user has the same application open in two tabs and each GET request generates a new CSRF token that replaces the previous one in the session, submitting a form from the first tab will fail because the session now holds the token from the second tab's page load. Per-session tokens (generated once and reused until the session ends) are immune to this problem. Per-request tokens are stronger against replay attacks but introduce multi-tab conflicts.

Diagnosing CSRF token validation failures

Start by confirming the exact error. A 403 with CSRF in the message is a validation failure, which means the server-side check ran and the tokens did not match. This is different from a 403 caused by a missing token, which may indicate the token is not being transmitted at all. Check your server logs for both error types.

For synchronizer token pattern, verify three things: first, that the token is being correctly embedded in the HTML (check the page source for a hidden input with name _csrf or an X-CSRF-Token meta tag); second, that the JavaScript code is correctly reading the token from the DOM (add a console.log of the token value before the request); third, that the token appears in the request as either a body parameter or a request header. Use DevTools Network panel to inspect the POST request body and headers.

For double-submit cookie pattern, verify that the csrf-token cookie is present in Application > Cookies, that the cookie is not httpOnly (otherwise JavaScript cannot read it), and that the request header X-CSRF-Token contains the same value as the cookie. If the values differ, check your cookie-reading JavaScript for encoding differences. Pay attention to URL-encoding: a cookie value containing + or % will appear differently if decoded before being placed in the header.

For load-balanced deployments, CSRF token validation can fail if the server that issued the token and the server that validates the request do not share session storage. Each server must use the same session store (Redis, a database, or a sticky session mechanism). If you see intermittent CSRF failures that correlate with request count or time of day (when a new server spins up), this is almost certainly the cause. Enable sticky sessions in your load balancer or migrate to a centralized session store.

How to fix CSRF token mismatch correctly

For form-based applications using server-rendered HTML, use the synchronizer token pattern. Generate the token once per session (not per request) and embed it as a hidden form field. When the session is regenerated on login (to prevent session fixation), also regenerate the CSRF token and ensure the client receives the new token before any subsequent form submissions. This is usually handled by redirecting to a new page after login, which loads the new token.

For single-page applications with a stateless JWT API, the double-submit cookie pattern is more appropriate. The server sets a non-httpOnly cookie with a random CSRF token on the response to the initial page load. The SPA JavaScript reads the cookie and includes it as the X-CSRF-Token header on every state-changing request. The server verifies that the header value matches the cookie value. Since the cookie is SameSite protected and the header value can only be read by same-origin JavaScript, cross-site attackers cannot replicate the header.

For APIs that use the SameSite=Strict or SameSite=Lax cookie attribute on session cookies, the browser itself provides CSRF protection for same-origin form submissions because cross-site requests will not include the session cookie. OWASP CSRF Cheat Sheet 2024 notes that SameSite=Lax is sufficient CSRF protection for most applications and recommends using it as a defense-in-depth layer alongside (not instead of) explicit CSRF tokens.

Security consideration: never disable CSRF protection for a route because it is called from a mobile app. Mobile apps do not use browser cookies and are not subject to CSRF, but the same endpoint may also be called from a web browser where CSRF is a real threat. Instead, use a separate authentication mechanism (Bearer tokens instead of cookies) for the mobile client and keep CSRF protection on the cookie-authenticated web path.

Edge cases in CSRF token validation

Browser back-navigation is a known edge case. When a user navigates back to a form page, the browser often restores the cached HTML page rather than re-fetching it from the server. If the cached page contains a CSRF token that is no longer valid (because the session was regenerated on login), submitting the form from the cached page will produce a token mismatch. The most user-friendly fix is to add a Cache-Control: no-store header to all form pages so the browser always fetches a fresh copy. Alternatively, detect the back-navigation in JavaScript using the Page Visibility API and refresh the CSRF token via an AJAX endpoint.

Cached static HTML is a related problem. If a CDN or proxy caches a page with an embedded CSRF token, all users who receive the cached version will share the same token value. When they submit the form, the server will compare the submitted token against the user's individual session token, which will not match the shared cached value. CSRF tokens must never be cached. Add Cache-Control: private, no-cache to pages containing CSRF tokens and ensure your CDN is configured to bypass caching for authenticated pages.

CSRF token length and entropy matter. OWASP recommends at least 128 bits (16 bytes) of cryptographically random data. Using Math.random() or a sequential counter instead of crypto.randomBytes() produces predictable tokens that an attacker could potentially guess. In Node.js, always use crypto.randomBytes(32).toString('hex') or an equivalent cryptographically secure random number generator.

Session regeneration on privilege escalation is required by OWASP to prevent session fixation attacks. When a user logs in, the session ID must change. This also means the CSRF token must change, because the old token was bound to the pre-login session. Ensure that your login handler regenerates the session, generates a new CSRF token, and redirects the user to a page that embeds the new token before any state-changing form is accessible.

Common CSRF implementation mistakes

The most dangerous mistake is verifying CSRF tokens only on POST requests but not on PUT, PATCH, and DELETE. CSRF attacks can forge any HTTP method that the browser will execute via a form or fetch(). Any state-changing request that relies on cookie authentication needs CSRF protection regardless of HTTP method.

Using a predictable CSRF token is a critical security flaw that is easy to miss. Some developers use the session ID as the CSRF token, or use a hash of the user ID and a static salt. If an attacker can predict or enumerate the token, the CSRF protection is bypassed. Always use a cryptographically random value that is independent of any other session data.

Emitting the CSRF token in the URL is a common mistake in older applications that did not support form submissions. URL-based tokens appear in browser history, server access logs, and Referer headers sent to third-party origins, all of which are potential sources of leakage. Never put CSRF tokens in URLs. Use hidden form fields or request headers exclusively.

Skipping CSRF validation for requests with an application/json content type is a misconception that appears frequently in security discussions. The reasoning is that cross-site attackers cannot send JSON (only form-encoded or multipart requests). This was partially true before browsers implemented fetch() with full CORS support. Today, a same-origin malicious page can use fetch() with a JSON body and credentials: include. Never rely on content type as a CSRF defense. Apply token validation to all state-changing requests regardless of content type.

OWASP CSRF best practices for production systems

OWASP CSRF Prevention Cheat Sheet 2024 recommends three complementary defenses layered together: a CSRF token (synchronizer token or double-submit cookie), the SameSite cookie attribute, and the Origin header verification check. Verifying that the Origin header matches an expected value provides additional protection when token-based CSRF is not feasible (e.g., for form submissions that cannot include custom headers).

For new applications, SameSite=Lax on session cookies is a strong baseline defense. Lax prevents cookies from being sent on cross-site POST requests (the primary CSRF attack vector) while allowing them on same-site GET navigations and top-level navigation from external links. Combine SameSite=Lax with explicit CSRF tokens for defense in depth. Do not rely on SameSite alone for high-risk operations like fund transfers or account deletion.

Per-session tokens are preferable to per-request tokens for most applications because they avoid multi-tab conflicts and do not require complex state management on the client. Per-request tokens provide slightly stronger protection against replay attacks but introduce significant UX complexity. OWASP notes that per-session tokens are acceptable when sessions have a reasonable timeout and the application regenerates tokens on login.

Document your CSRF implementation explicitly in your security runbook. Include which pattern is used, which routes are protected, what the token lifetime is, and how the token is invalidated. During a security audit, auditors will look for this documentation to verify coverage. Also add automated tests that verify a POST request without a CSRF token returns 403, and that a request with a forged token value also returns 403.

Quick fix checklist

  • Verify the CSRF token is present in the page source as a hidden field or meta tag before form submission
  • Confirm the token is transmitted in the request body field or X-CSRF-Token header, never in the URL
  • Check that the token value in the request matches what is stored in the session or cookie
  • Set session cookies with SameSite=Lax or SameSite=Strict as an additional CSRF defense layer
  • Confirm the session store is shared across all servers in a load-balanced deployment
  • Add Cache-Control: private, no-cache to all pages containing CSRF tokens
  • Regenerate the CSRF token when the session is regenerated on user login
  • Apply CSRF protection to PUT, PATCH, and DELETE routes, not just POST

Related guides

Frequently asked questions

Why do I get a CSRF token mismatch only after being idle for a while?

The most common cause is session expiry. The CSRF token is tied to the session. If the session expires while the form is open, the token stored server-side is gone. When the user submits, the server cannot find the session to compare the token against and returns a mismatch error. Extend the session lifetime, use rolling sessions, or implement a graceful re-authentication flow that preserves form data.

Can I skip CSRF protection if my API uses JWT Bearer tokens?

Yes, if the API uses only Bearer tokens in the Authorization header and no cookies. Browser CSRF attacks work by exploiting automatic cookie inclusion. If your API does not accept cookies for authentication, there is no cookie for the attacker to exploit. However, if the same API endpoint accepts both cookie and bearer authentication, CSRF protection is still needed for the cookie-authenticated path.

Is SameSite=Strict sufficient CSRF protection on its own?

SameSite=Strict is very strong CSRF protection because the session cookie is never sent on any cross-site request, including top-level navigations from external links. However, OWASP recommends using it as defense-in-depth alongside explicit CSRF tokens, not as a replacement. Some older browsers do not support SameSite and would still be vulnerable without a token-based fallback.

What is the difference between per-session and per-request CSRF tokens?

Per-session tokens are generated once when the session starts and remain valid until the session ends. Per-request tokens are regenerated after each state-changing request, providing stronger replay protection. Per-session tokens are easier to implement and avoid multi-tab conflicts. OWASP considers per-session tokens acceptable for most applications. Per-request tokens are recommended only for very high-security operations like banking transactions.

Why does my CSRF token fail only in certain browsers?

Different browsers implement the SameSite cookie default differently. Older browsers (IE11, Safari before version 12) do not support SameSite and send cookies on all cross-site requests. If your CSRF protection relies solely on SameSite behavior, it will appear to work in Chrome 80+ but fail in older browsers. Ensure you also use explicit CSRF tokens for full cross-browser protection.

How do I implement CSRF protection in a single-page application?

For SPAs, use the double-submit cookie pattern. The server sets a non-httpOnly cookie with a random CSRF token on the initial page load. The SPA JavaScript reads this cookie via document.cookie and includes it as the X-CSRF-Token header on every state-changing request. The server verifies the header matches the cookie. Axios can be configured to send this header automatically via interceptors.

Is the csurf npm package safe to use?

The csurf package was deprecated in 2023 due to maintenance concerns, though the underlying pattern is still valid. For new applications, use csrf-csrf or csrf-sync which are actively maintained alternatives. For existing applications using csurf in Express, the package still works but will not receive security updates. Plan a migration to a maintained package.

What entropy should a CSRF token have?

OWASP recommends at least 128 bits (16 bytes) of cryptographically random data for CSRF tokens. In Node.js, use crypto.randomBytes(32).toString('hex') which generates 32 random bytes encoded as 64 hex characters, providing 256 bits of entropy. Never use Math.random(), sequential IDs, or any deterministic value derived from user-identifiable data.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.