CSRF: The Attack That Makes Your Users Hack Themselves ðŠĪ
Imagine you're logged into your bank. You check your email, click a link from a "totally legitimate" newsletter, and â without typing a single thing â you just transferred $5,000 to someone you've never met. You didn't do it. But your browser did. Gleefully. On your behalf.
Welcome to Cross-Site Request Forgery (CSRF), the attack that weaponizes trust. Not your trust in a shady website â the server's trust in you.
How CSRF Actually Works ð§
Browsers have a quirk that attackers love: when your browser sends a request to yourbank.com, it automatically includes any cookies it has for that domain. Session cookies, auth cookies â all of it. The server sees a valid session and says, "Oh, it's you! Right this way!"
CSRF exploits this by tricking your authenticated browser into making a request you never intended.
Here's the classic attack flow:
- You log into
yourbank.com. A session cookie is set. - You visit
evil-memes.com(in the same browser, different tab). - That page silently loads this:
<!-- Hidden on evil-memes.com -->
<img src="https://yourbank.com/transfer?to=hacker&amount=5000" style="display:none">
- Your browser fetches that "image" URL â with your session cookie attached.
yourbank.comsees a valid session and processes the transfer.- You are now $5,000 poorer and very confused.
No phishing form. No stolen password. Just your own authenticated session, turned against you. ð
The POST Version (It Gets Worse) ðŪ
GET-based attacks are obvious, so most developers protect against them. But POST requests? That's where things get spicy.
<!-- The attacker embeds this on their site -->
<form id="evil-form" action="https://yourbank.com/transfer" method="POST">
<input type="hidden" name="to" value="hacker_account">
<input type="hidden" name="amount" value="5000">
</form>
<script>
// Auto-submits the moment you visit the page
document.getElementById('evil-form').submit();
</script>
The user visits the attacker's page, the form silently submits in the background, and the bank's server happily processes it â because the session cookie rode along for free.
This is why "just use POST instead of GET" is not a security strategy. ðĪĶ
How to Stop It: The CSRF Token Pattern ðĄïļ
The fix is elegant: make every sensitive request prove it came from your page, not some random site.
Server-side (Node.js + Express example):
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use(csrf({ cookie: true }));
// Inject the token into every rendered page
app.get('/transfer', (req, res) => {
res.render('transfer', {
csrfToken: req.csrfToken() // Unique, unpredictable token per session
});
});
// The middleware automatically validates it on POST
app.post('/transfer', (req, res) => {
// If req.body._csrf doesn't match the session token â 403 Forbidden
// If it matches â process the transfer
processTransfer(req.body);
});
Client-side (your HTML form):
<form action="/transfer" method="POST">
<!-- The hidden field carries the proof-of-origin token -->
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<input type="text" name="to" placeholder="Recipient account">
<input type="number" name="amount" placeholder="Amount">
<button type="submit">Transfer</button>
</form>
What just happened?
- The token is generated server-side and tied to your session.
- It's embedded in your legitimate page.
- An attacker on
evil-memes.comcan't read it (same-origin policy blocks cross-origin reads). - When the POST arrives without a valid token â rejected. When it arrives with one â trusted.
The attacker's form has no token. Request denied. Attack foiled. ð
Modern Defense: SameSite Cookies ðŠ
If you're building something new, SameSite cookies are your best friend:
// In your session/cookie config
res.cookie('sessionId', token, {
httpOnly: true, // No JS access
secure: true, // HTTPS only
sameSite: 'Strict', // THE CSRF KILLER
maxAge: 3600000
});
SameSite: Strict tells the browser: "Only send this cookie if the request originated from my own site."
Cross-site form submission? No cookie. API fetch from evil-memes.com? No cookie. The bank never sees an authenticated request. Attack dead on arrival. ð
| SameSite Value | Behavior | CSRF Protection |
|---|---|---|
Strict |
Cookie never sent cross-site | Full protection |
Lax |
Sent on top-level navigations (links) | Partial (blocks form POSTs) |
None |
Always sent (old behavior) | None â you're on your own |
Gotcha: SameSite: Strict breaks OAuth flows and "login with Google" style redirects. Lax is the pragmatic default for most apps â it blocks form-based CSRF while keeping login flows working.
APIs and CSRF: Are You Actually Safe? ðĪ
"I use a REST API with JWT in the Authorization header â I'm fine, right?"
Mostly yes. If your API only accepts tokens in the Authorization: Bearer <token> header (not cookies), CSRF doesn't apply. Attackers can't set custom headers cross-site.
But if your API falls back to cookies for authentication? You're back in CSRF territory.
The quick gut-check:
- Auth via
Authorizationheader only â not CSRF vulnerable - Auth via session cookies â add CSRF protection
- Auth via cookies and allows header-less auth â definitely add CSRF protection
The "Double Submit Cookie" Pattern (For SPAs) âĄ
Single-page apps often can't use traditional CSRF tokens because they don't have server-rendered HTML. Here's the SPA-friendly approach:
- Server sets a non-httpOnly CSRF cookie (so JavaScript CAN read it).
- Client JavaScript reads the cookie and sends it as a custom header.
- Server verifies the header value matches the cookie.
An attacker's cross-site request can't set custom headers, so even if the cookie rides along, the header won't â and the check fails.
// Client-side (React/Vue/Angular)
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf-token='))
?.split('=')[1];
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken, // Attacker can't set this cross-site!
},
body: JSON.stringify({ to: 'friend', amount: 50 })
});
Your CSRF Defense Checklist â
Before you ship:
- Using
SameSite=Lax(orStrict) on all auth cookies - CSRF tokens on all state-changing forms (POST, PUT, DELETE, PATCH)
- API endpoints verify
Authorizationheader, not just cookies - No sensitive actions triggered by GET requests
- CORS configured to reject unexpected origins
- Security headers include
Referer/Originvalidation as a backup layer
Real Talk: Is CSRF Still a Threat in 2026? ðŽ
Yes, but the landscape has shifted. Modern browsers default SameSite to Lax for new cookies, which kills most naive CSRF attacks. But:
- Legacy apps still explicitly set
SameSite=None - Cookie misconfiguration is extremely common
- Some attack vectors (like login CSRF) still work even with
Lax - It's still #5 on the OWASP API Security Top 10
The attacks got harder. The bugs didn't disappear.
Quick Wins (Do These Today) ð
- Audit your cookie flags â search your codebase for
cookiesettings and addsameSite: 'lax' - Add csurf (or equivalent) to your Express/Laravel/Django app â 15-minute task, massive payoff
- Check your API authentication â make sure it's header-based, not cookie-dependent
- Test it yourself â open DevTools, copy your session cookie, try replaying a request from a different origin
The Bottom Line
CSRF is one of those attacks that feels impossible until you understand it, then feels obvious once you do. The browser's automatic cookie behavior â normally a convenience â becomes a liability when the server trusts it too much.
The good news: it's one of the most preventable vulnerabilities in web security. One well-placed token or one cookie flag change, and you've closed the door completely.
Your users deserve to make their own decisions about where their $5,000 goes. ðĄïļ
Building something and want a second set of eyes on your auth flow? Connect on LinkedIn â always happy to talk security.
Found this useful? Share it with a dev who's still using GET requests for sensitive actions. You might save them from an awkward incident report. ð
P.S. â Go check your cookies right now. If you don't see SameSite anywhere, that's your homework. ðŠð