The Synchronizer Token Pattern (STP)
The Synchronizer Token Pattern is the gold-standard application-level defense against CSRF. When a user establishes a session, the server generates a cryptographically strong pseudo-random token and binds it to the user's session.
When rendering forms or issuing API mutations, the server injects this token into the client (via a hidden form field or custom HTTP header). When processing state-changing requests (POST, PUT, DELETE), the server compares the submitted token against the session token. Because the Same-Origin Policy prevents attacker.com from reading tokens on bank.com, the attacker cannot forge a valid request.
// Node.js Express CSRF Token Verification Middleware
import crypto from 'crypto';
import type { Request, Response, NextFunction } from 'express';
export function verifyCsrfToken(req: Request, res: Response, next: NextFunction) {
// Safe HTTP methods do not change state
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const clientToken = req.headers['x-csrf-token'] || req.body?._csrf;
const sessionToken = req.session?.csrfToken;
if (!clientToken || !sessionToken || !crypto.timingSafeEqual(Buffer.from(clientToken), Buffer.from(sessionToken))) {
return res.status(403).json({ error: 'CSRF token verification failed' });
}
next();
}Why CORS Does NOT Protect Against CSRF
A frequent and dangerous misconception among developers is believing that Cross-Origin Resource Sharing (CORS) prevents CSRF attacks.
CORS is a browser mechanism that restricts whether a web page can read the response of a cross-origin request. However, CSRF is an attack on state execution, not data retrieval. When a malicious form submits a POST request cross-site, the browser dispatches the request and the server executes the database mutation - even if CORS blocks the attacker from reading the response! CORS does not replace anti-CSRF defenses.
The Chromium 2-Minute 'Lax-allowing-unsafe' Exception
A critical security nuance in modern Chromium-based browsers (Chrome, Edge, Brave, Opera) is the 2-minute Lax exception:
When a cookie is created without an explicit SameSite attribute, Chromium defaults to SameSite=Lax. However, to prevent breaking legacy single sign-on (SSO) redirect flows, Chromium implements a temporary exemption: for the first 120 seconds after creation, top-level cross-site POST requests are permitted to include the cookie!
This means an attacker who induces a victim to click a link within 2 minutes of logging in can successfully execute CSRF if your application relies solely on default SameSite settings. Explicit cryptographic Anti-CSRF tokens or custom request headers (`X-Requested-With`) remain strictly mandatory. You can audit CSRF defenses across your endpoints using the Wescequre Vulnerability Explorer.
Common Mistakes to Avoid
❌Performing state-changing actions via GET requests (e.g., `/user/delete?id=5`)
Why it happens: Developers create simple links for convenience instead of forms or POST requests.
Why it matters: GET requests can be triggered by simple `<img>` tags (`<img src='bank.com/transfer?amount=100'>`), completely bypassing `SameSite=Lax` protection.
Correct approach: Strictly adhere to HTTP specifications: GET, HEAD, and OPTIONS must be safe and idempotent. All mutations must use POST, PUT, PATCH, or DELETE.
❌Assuming CORS configuration protects against CSRF
Why it happens: Developers see 'CORS error' in the console and assume the request was blocked.
Why it matters: CORS blocks the client from reading the response; the server still receives, parses, and executes the state-changing request.
Correct approach: Implement SameSite cookies and anti-CSRF tokens regardless of CORS configuration.
❌Using static or predictable CSRF tokens
Why it happens: Developers derive tokens from user IDs or timestamps.
Why it matters: Predictable tokens allow attackers to calculate the required token and forge requests successfully.
Correct approach: Generate tokens using a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG, e.g., `crypto.randomBytes(32)`).
❌Comparing CSRF tokens with standard equality operators (`===`)
Why it happens: Developers write `if (clientToken === sessionToken)`.
Why it matters: Standard string comparisons terminate on the first non-matching byte, exposing the server to timing attacks.
Correct approach: Use constant-time comparison functions like `crypto.timingSafeEqual()`.
Troubleshooting Guide
Problem: Legitimate users receive 403 CSRF verification failed after opening multiple browser tabs
Possible Causes:
- The server generates a new CSRF token on every page load and overwrites the session token, invalidating tokens in earlier tabs.
How to verify: Open two tabs, submit a form in tab 1, and check for 403 Forbidden.
How to fix: Bind a single CSRF token per user session rather than per request, or implement token rotation with grace periods.
Problem: Mobile app or external API clients fail with CSRF errors
Possible Causes:
- CSRF middleware is applied globally to all API endpoints, including non-browser clients.
How to verify: Send a request via curl or Postman and inspect for CSRF validation failures.
How to fix: Only enforce CSRF checks on requests authenticated via session cookies; API clients using `Authorization: Bearer <JWT>` are not vulnerable to CSRF.
Actionable Checklist
Wescequre Platform · Scanner Engine
Automated Vulnerability Scanner
Launch targeted or full-domain security audits to detect OWASP Top 10 vulnerabilities automatically.
Includes: OWASP Top 10 automated test suites · Real-time severity scoring (CVSS v3.1) · Instant remediation code snippets
Frequently Asked Questions
Standard cross-origin HTML forms can only send `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`. However, modern browsers can send cross-origin `application/json` via `fetch()` if CORS is misconfigured. Requiring custom headers (like `X-CSRF-Token`) triggers a browser CORS preflight check, effectively blocking CSRF.
If the JWT is stored in an `HttpOnly` cookie, the application IS vulnerable to CSRF and requires anti-CSRF tokens. If the JWT is stored in memory and sent via the `Authorization: Bearer <token>` header, it is immune to CSRF (though vulnerable to XSS if tokens are exposed).
Chromium introduced a temporary mitigation where cookies without a SameSite attribute default to `Lax`, but permit cross-site POST requests within 2 minutes of creation ('Lax+POST'). Explicitly specifying `SameSite=Lax` or `SameSite=Strict` prevents this behavior.
Authoritative Sources & References
- OWASP Cross-Site Request Forgery Prevention Cheat SheetOWASP Foundation (official)View Source
- IETF RFC 6265bis: Cookies: HTTP State Management MechanismIETF (official)View Source
- MDN Web Docs: SameSite cookiesMozilla (technical)View Source
- IETF RFC 6265bis: SameSite Cookie Attribute SpecificationIETF (official)View Source
Related Guides
Continue exploring related technical architecture and defensive guides
sessions · cookies
Secure Session Management: Cookies, Tokens & Best Practices
Technical guide to session security: cryptographically secure cookie prefixes (`__Host-`), session fixation defense, idle timeouts, and Redis session architectures.
authentication · passwords
Secure Authentication Architecture: Passwords, MFA & Passkeys
Developer blueprint for authentication architecture: Argon2id password hashing parameters, FIDO2/WebAuthn passkey implementation, and credential stuffing defense.
API security · REST
REST API Security Best Practices for Modern Applications
Developer blueprint for securing REST APIs: BOLA/IDOR prevention patterns, asymmetric JWT validation, Redis token bucket rate limiting, and strict input DTOs.
