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.