Understanding CSRF: Why Cookies Aren't Enough
The Problem
Imagine you're logged into bank.com. While browsing the web, you visit evil.com which contains this innocent-looking link:
When you click, your browser sends the request to bank.com with your authentication cookies automatically attached. The bank sees a legitimate session and processes the transfer. You've been attacked without knowing it.
This is Cross-Site Request Forgery (CSRF).
How It Works: The Cookie Problem
The vulnerability exists because browsers automatically include cookies with every request to a domain, regardless of where the request originated.
You visit evil.com
β
evil.com triggers: POST https://bank.com/transfer
β
Browser automatically sends: Cookie: sessionId=abc123
β
bank.com sees valid session β Processes request β
The server can't tell the difference between:
A legitimate request from bank.com/transfer-page
A forged request from evil.com
Both include the same authentication cookies.
The Solution: CSRF Tokens
The defense adds a second layer: a CSRF token that evil.com cannot obtain.
How It Works
Server generates a unique, random token for each user session
Token is stored server-side (tied to the user's session)
Token is sent to the client (in HTML or sessionStorage)
Client includes token in every sensitive request
Server validates: "Does the token in the request match the user's session token?"
Example Flow
Legitimate request:
// bank.com loads β stores token in sessionStorage
sessionStorage.setItem('csrf_token', 'abc123xyz');
// User submits form
fetch('/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': sessionStorage.getItem('csrf_token') // "abc123xyz"
},
body: JSON.stringify({to: 'friend', amount: 100})
});
// Server validates:
// - Cookie: sessionId=user_1 β Session has token: "abc123xyz"
// - Header: X-CSRF-Token: "abc123xyz"
// - Match! β
Process request
Attack attempt:
// evil.com tries to forge request
fetch('https://bank.com/transfer', {
method: 'POST',
body: JSON.stringify({to: 'attacker', amount: 1000})
});
// Browser sends cookies automatically
// But NO csrf_token header (evil.com doesn't know the token)
// Server validates:
// - Cookie present β
// - CSRF token? β Missing or wrong
// - REJECT! (403 Forbidden)
Why attackers can't steal the token:
The token is protected by the Same-Origin Policy:
evil.com can't read bank.com's sessionStorage (browser isolates storage per domain)
evil.com can't fetch bank.com pages and read the response (CORS blocks it)
Token is unpredictable (randomly generated, unique per session)
Without the token, the forged request fails.
Diagram: CSRF Attack vs. Defense

CSRF vs. CORS: What's the Difference?
Common confusion: "Don't CORS protections prevent CSRF?"
No. They protect different things:
| CSRF | CORS |
| Prevents unwanted actions | Controls reading responses |
| Attacker triggers request to harm you | Attacker tries to read your data |
| Defense: CSRF tokens | Defense: CORS headers |
Key Insight
CORS doesn't prevent CSRF because:
CORS blocks reading cross-origin responses
CSRF attacks don't need to read the response
The damage is done when the request executes (transfer money, delete account, etc.)
Even with CORS errors in the console, the CSRF attack succeeds:
Browser console on evil.com: β CORS error: Response blocked
Network tab: β POST /transfer - 200 OK
Result: Money transferred, but evil.com can't read the response
Conclusion
CSRF exploits the browser's automatic cookie behavior. The defense is simple but powerful:
Cookies alone = Authentication only ("Who is making the request?")
Cookies + CSRF Token = Authentication + Intent verification ("Who is making the request AND did they really mean to?")
Modern best practices:
Use CSRF tokens for all state-changing requests (POST, PUT, DELETE)
Set SameSite=Lax on cookies (modern browsers default to this)
Store tokens in sessionStorage for SPAs (cleared when tab closes)
Never rely on cookies alone for sensitive actions
Remember: If a request can change data, protect it with a CSRF token.