CSRF Prevention
When to Use
Protect ALL state-changing operations: POST/PUT/DELETE requests, password changes, money transfers, profile updates, admin actions. GET requests should NEVER change state (by design, GET = read-only).
How CSRF Attacks Work
1. User logs into bank.com, receives session cookie
2. User visits evil.com (different tab, same browser)
3. evil.com contains hidden form:
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker_account">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
4. Form submits to bank.com with user's session cookie (automatic)
5. Bank processes transfer as legitimate request (user is authenticated)
6. Money transferred to attacker
Why it works: Browsers automatically include cookies with requests to matching domains, even from other sites.
Decision
| If you need to... | Use... | Why |
|---|---|---|
| CSRF protection (modern apps) | SameSite cookies + CSRF tokens | Double protection |
| CSRF protection (legacy browsers) | CSRF tokens (synchronizer token pattern) | SameSite not supported in old browsers |
| API authentication | Token-based auth (JWT, API keys) instead of cookies | No automatic credential inclusion |
| Simple protection | SameSite=Lax cookies | Blocks CSRF for POST/PUT/DELETE (not GET) |
| Strict protection | SameSite=Strict cookies | Blocks CSRF completely (but breaks legitimate cross-site navigation) |
Pattern
SameSite cookies (primary defense for 2025+):
// PHP - SameSite=Lax (recommended default)
setcookie('session_id', $session_value, [
'httponly' => true,
'secure' => true,
'samesite' => 'Lax', // Browser won't send cookie on POST from other sites
]);
// Express.js (Node.js)
app.use(session({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax'
}
}));
CSRF tokens (defense in depth):
# Django - automatic CSRF protection
<form method="POST">
{% csrf_token %}
<input name="email" value="{{ user.email }}">
<button type="submit">Update</button>
</form>
# Django validates token automatically - returns 403 if missing/invalid
// PHP - manual CSRF token implementation
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="POST" action="/update-profile">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit">Update</button>
</form>
<?php
// Validate token on submission
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token validation failed');
}
// React SPA - CSRF token in header
function updateProfile(email) {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
return fetch('/api/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ email })
});
}
Double submit cookie pattern (stateless CSRF protection):
// Server sets CSRF token as cookie
response.cookie('csrf_token', token, {
httpOnly: false, // JavaScript must read it
secure: true,
sameSite: 'lax'
});
// Client reads cookie and submits as header
const csrfToken = getCookie('csrf_token');
fetch('/api/update', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken },
body: JSON.stringify(data)
});
// Server validates: header value === cookie value
// Attacker can't read cookies from other domains (same-origin policy)
Defense Layers
Recommended 2025+ approach (multiple layers):
- SameSite=Lax cookies — blocks most CSRF attacks
- CSRF tokens — catches edge cases (SameSite bypass, old browsers)
- Origin/Referer validation — supplementary check
- Re-authentication for sensitive actions — password change, money transfer require password re-entry
Common Mistakes
- Using only SameSite cookies — SameSite has edge cases and browser inconsistencies. Use tokens too
- CSRF protection on GET requests — GET should never change state. If your GET endpoints modify data, that's the real problem
- Checking Referer header only — Referer can be stripped by proxies, privacy extensions, HTTPS to HTTP transitions
- Not protecting JSON APIs — CSRF affects JSON APIs too. SameSite cookies + custom header required
- Weak token generation — Use cryptographically secure random. NOT Math.random() or predictable patterns
- Token reuse across sessions — Generate new token per session or per request
- SameSite=None without reason —
SameSite=Nonedisables protection. Only use for legitimate cross-site embedding
See Also
- Previous: SQL Injection Prevention | Next: Authentication Best Practices
- Reference: OWASP CSRF Prevention Cheat Sheet
- Reference: SameSite cookies explained