Classic Form CSRF
Classic form CSRF: HTML form on attacker.com posts to victim.com with the victim’s session cookie attached. The defence is a server-issued, server-verified anti-forgery token in the form body.
The attack + the standard cure
EXAMPLE
<!-- THE ATTACK — hosted on attacker.example -->
<!doctype html>
<html>
<body onload="document.forms[0].submit()">
<form action="https://bank.example/api/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account_id">
<input type="hidden" name="amount" value="5000">
</form>
</body>
</html>
<!--
Victim is logged in to bank.example.
Browser auto-sends bank.example's session cookie on the cross-site POST.
Without CSRF defences, server processes the transfer.
-->
<!-- THE CURE — anti-forgery token per session -->
<!-- Server-side: Express + a tiny token store -->
import crypto from 'node:crypto';
function issueCsrfToken(req) {
req.session.csrf ??= crypto.randomBytes(32).toString('hex');
return req.session.csrf;
}
function verifyCsrf(req, res, next) {
const sent = req.body?._csrf || req.headers['x-csrf-token'];
if (!sent || sent !== req.session.csrf) return res.status(403).end();
next();
}
app.get('/transfer', (req, res) => {
const token = issueCsrfToken(req);
res.render('transfer', { csrf: token });
});
app.post('/transfer', verifyCsrf, async (req, res) => {
/* … move money … */
});
<!-- Template -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{ csrf }}">
<input name="to">
<input name="amount">
<button>Send</button>
</form>
<!-- Defence in depth — pair the token with SameSite cookies + Origin check -->
Set-Cookie: sid=…; HttpOnly; Secure; SameSite=Lax
// Express middleware
app.use((req, res, next) => {
if (['POST','PUT','PATCH','DELETE'].includes(req.method) &&
!ALLOWED_ORIGINS.has(req.headers.origin)) {
return res.status(403).end();
}
next();
});
Why it matters
Three layers stop classic form CSRF: SameSite cookies (most browsers default to Lax), an anti-forgery token in the form, and an Origin check on the server. Skip any one and the attack works.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// VULNERABLE: state-changing GET / unauthenticated POST // SAFE: only mutate via POST/PUT/DELETE, require a CSRF token in the body, // validate it server-side against the session.Try it Yourself »
Discussion
Loading…