CSRF Protection
Protect web applications against cross-site request forgery (CSRF) using tokens, SameSite cookies, and origin validation.
You are an expert in defending web applications against cross-site request forgery (CSRF) attacks using token-based, header-based, and cookie-based mitigations. ## Key Points 1. User authenticates on `bank.example.com` and receives a session cookie. 2. User visits `evil.example.com`, which contains a hidden form targeting `bank.example.com/transfer`. 3. The browser attaches the session cookie automatically. 4. The bank processes the forged request as if the user initiated it. - **`Strict`**: Cookie never sent on cross-site requests. Strongest protection but breaks legitimate cross-site navigation (e.g., links from email). - **`Lax`**: Cookie sent on top-level GET navigations but not on POST or subresource requests. Good default balance. - **`None`**: Cookie sent on all cross-site requests (requires `Secure` flag). Offers no CSRF protection. 1. **Use SameSite=Lax as a baseline**: It stops most CSRF without breaking normal navigation. Combine with token-based protection for defense in depth. 2. **Require a custom header for APIs**: Browsers do not allow cross-origin requests to set custom headers without a preflight. Requiring `X-CSRF-Token` or similar blocks simple CSRF. 3. **Generate cryptographically random tokens**: Use `crypto.randomBytes` or equivalent, never predictable values. 4. **Validate on every state-changing request**: Apply CSRF checks to POST, PUT, PATCH, DELETE — never only on specific routes. 5. **Rotate tokens per session**: Generate a new token when a session is created or when the user authenticates.
skilldb get security-practices-skills/CSRF ProtectionFull skill: 247 linesCSRF Protection — Application Security
You are an expert in defending web applications against cross-site request forgery (CSRF) attacks using token-based, header-based, and cookie-based mitigations.
Core Philosophy
CSRF protection is rooted in a single question: "Did this request genuinely come from my application, or was the user's browser tricked into sending it?" The browser automatically attaches cookies to every request for a given domain, which means authentication alone cannot distinguish between a legitimate user action and a forged one. CSRF defenses exist to close this gap by requiring proof of intent that only your own application can provide.
Defense in depth is essential because no single CSRF mitigation is bulletproof in isolation. SameSite cookies depend on browser support and do not cover every scenario. Tokens can leak through Referer headers or logging. Origin validation can be stripped by proxies. The strongest posture layers multiple defenses — SameSite cookies as a baseline, synchronizer tokens for form submissions, and custom headers for API calls — so that a weakness in one layer is covered by another.
The most effective CSRF strategies align with the principle of least surprise: state-changing operations use POST (never GET), tokens are scoped to the session and rotated on authentication, and developers never need to remember to "opt in" to protection because the middleware enforces it globally. When CSRF protection is an afterthought bolted onto individual routes, gaps are inevitable.
Anti-Patterns
-
Using GET requests for state-changing operations: Links, image tags, prefetch hints, and browser extensions can all trigger GET requests silently. Any action that modifies data — transfers, deletions, settings changes — must use POST, PUT, PATCH, or DELETE.
-
Relying solely on SameSite cookies: While SameSite=Lax is an excellent baseline, it does not protect against subdomain attacks, and older browsers ignore it entirely. Treating it as the only defense leaves gaps that an attacker can exploit.
-
Embedding CSRF tokens in URLs: Tokens placed in query strings leak through Referer headers, browser history, server logs, and analytics tools. Always transmit tokens in POST bodies or custom request headers.
-
Exempting AJAX routes from CSRF validation: Developers sometimes assume that because AJAX requests require JavaScript, they cannot be forged cross-origin. This is false if CORS is misconfigured or if the attacker uses a form submission that mimics the API call format.
-
Sharing CSRF tokens across sessions or users: A CSRF token must be tied to a specific session. Reusing tokens across sessions, caching them in shared layers, or failing to regenerate them after login undermines the entire mechanism.
Overview
CSRF exploits the trust a server places in a user's browser. If a user is authenticated, an attacker can trick their browser into sending forged requests (form submissions, API calls) to the target site. The server processes them as legitimate because the session cookie is automatically attached. Defenses center on verifying that a request genuinely originated from your own site.
Core Concepts
How CSRF Works
- User authenticates on
bank.example.comand receives a session cookie. - User visits
evil.example.com, which contains a hidden form targetingbank.example.com/transfer. - The browser attaches the session cookie automatically.
- The bank processes the forged request as if the user initiated it.
Defense Layers
| Defense | Mechanism |
|---|---|
| Synchronizer Token | Unique per-session or per-request token in forms |
| Double-Submit Cookie | Token in cookie + request body/header must match |
| SameSite Cookie Attribute | Browser refuses to send cookie on cross-site requests |
| Origin/Referer Validation | Server checks Origin or Referer header |
| Custom Request Headers | APIs require a custom header that forms cannot set |
SameSite Cookie Values
Strict: Cookie never sent on cross-site requests. Strongest protection but breaks legitimate cross-site navigation (e.g., links from email).Lax: Cookie sent on top-level GET navigations but not on POST or subresource requests. Good default balance.None: Cookie sent on all cross-site requests (requiresSecureflag). Offers no CSRF protection.
Implementation Patterns
Synchronizer Token Pattern (Express + csurf Alternative)
const crypto = require('crypto');
// Generate and store a CSRF token in the session
function generateCsrfToken(session) {
const token = crypto.randomBytes(32).toString('hex');
session.csrfToken = token;
return token;
}
// Middleware to validate CSRF token
function csrfProtection(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const token = req.body._csrf
|| req.headers['x-csrf-token']
|| req.query._csrf;
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
// In the route handler, embed the token in forms
app.get('/transfer', (req, res) => {
const token = generateCsrfToken(req.session);
res.render('transfer', { csrfToken: token });
});
app.use(csrfProtection);
<!-- In the form template -->
<form method="POST" action="/transfer">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<input type="text" name="amount" />
<button type="submit">Transfer</button>
</form>
Double-Submit Cookie Pattern
const crypto = require('crypto');
// Set a CSRF cookie (not HttpOnly so JS can read it)
function setCsrfCookie(res) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
sameSite: 'Strict',
secure: true,
httpOnly: false, // client JS must read this
path: '/',
});
return token;
}
// Middleware: compare cookie value to header value
function doubleSubmitCheck(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
next();
}
// Client-side: read cookie and attach header
function getCookie(name) {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf-token'),
},
body: JSON.stringify({ amount: 100 }),
});
SameSite Cookie Configuration
// Express session with SameSite
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true, // requires HTTPS
sameSite: 'Lax', // or 'Strict' for maximum protection
maxAge: 3600000,
},
resave: false,
saveUninitialized: false,
}));
# Django settings
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
Origin / Referer Header Validation
function originCheck(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const origin = req.headers['origin'] || req.headers['referer'];
const allowedOrigins = ['https://myapp.example.com'];
if (!origin) {
return res.status(403).json({ error: 'Missing origin header' });
}
const parsed = new URL(origin);
if (!allowedOrigins.includes(parsed.origin)) {
return res.status(403).json({ error: 'Invalid origin' });
}
next();
}
Django Built-In CSRF
# Django has CSRF middleware enabled by default
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # built-in
# ...
]
# In templates, use the {% csrf_token %} tag:
# <form method="post">
# {% csrf_token %}
# ...
# </form>
# For AJAX, read the csrftoken cookie and set X-CSRFToken header
# Django checks this automatically
Best Practices
- Use SameSite=Lax as a baseline: It stops most CSRF without breaking normal navigation. Combine with token-based protection for defense in depth.
- Require a custom header for APIs: Browsers do not allow cross-origin requests to set custom headers without a preflight. Requiring
X-CSRF-Tokenor similar blocks simple CSRF. - Generate cryptographically random tokens: Use
crypto.randomBytesor equivalent, never predictable values. - Validate on every state-changing request: Apply CSRF checks to POST, PUT, PATCH, DELETE — never only on specific routes.
- Rotate tokens per session: Generate a new token when a session is created or when the user authenticates.
- Reject requests with missing Origin/Referer: For state-changing requests, require at least one of these headers and validate it.
- Do not use GET for state changes: GET requests bypass most CSRF defenses and are cached/logged/prefetched by browsers.
Common Pitfalls
- Relying solely on SameSite cookies: Older browsers do not support SameSite. Use tokens as well.
- Excluding AJAX routes from CSRF checks: Attackers can submit cross-origin requests with
fetchorXMLHttpRequestif CORS is misconfigured. Always validate. - Leaking CSRF tokens in URLs: Tokens in query strings end up in server logs, Referer headers, and browser history. Use POST bodies or custom headers.
- Not regenerating tokens after login: Session fixation can carry over a known CSRF token. Regenerate on authentication.
- Using GET requests for actions: Links, image tags, and prefetch mechanisms can trigger GET requests silently.
- Accepting tokens from any source without priority: Define a clear precedence (header > body > query) and reject if the token source is ambiguous.
Install this skill directly: skilldb add security-practices-skills
Related Skills
Content Security Policy
Configure Content-Security-Policy headers to mitigate XSS, data injection, and clickjacking attacks.
CORS Security
Configure CORS headers correctly to control cross-origin resource access while preventing overly permissive policies.
Input Validation
Validate and sanitize all user input at application boundaries using schemas, type coercion, and allowlists.
Secrets Management
Securely store, access, rotate, and audit application secrets and credentials using vaults, environment variables, and CI/CD integrations.
SQL Injection
Prevent SQL injection attacks using parameterized queries, ORM best practices, and input validation layers.
Supply Chain Security
Secure your software supply chain by auditing dependencies, pinning versions, verifying integrity, and monitoring for vulnerabilities.