Juicy Shop
Cross-Site Request Forgery (CSRF)
CSRF tricks a logged-in user's browser into sending a state-changing request they never intended. Learn why cookies make it possible and how SameSite cookies plus anti-CSRF tokens shut it down.
How the attack works
Browsers attach cookies to every request to a site automatically — including requests triggered by a different, malicious site. If your app authorises an action purely because a valid session cookie is present, an attacker can host a page that auto-submits a form (or fires fetch) to your endpoint. The victim's browser sends their cookies, and your server happily processes the forged request as if the user meant it.
What makes an endpoint vulnerable
Any state-changing request (transfer funds, change email, post a review, delete an account) that relies only on an ambient cookie for authorisation is at risk. GET requests that change state are especially dangerous because they can be triggered by a mere <img> tag. The first rule is therefore: never mutate state on GET.
Defence 1: SameSite cookies
Set session cookies to SameSite=Lax (a good default) or SameSite=Strict. With Lax, the cookie is withheld from cross-site sub-requests and cross-site POSTs, which blocks the classic auto-submitting-form attack while still allowing top-level navigations. SameSite is a strong, low-effort baseline, but treat it as defence in depth rather than your only control.
Defence 2: anti-CSRF tokens
Issue a per-session (or per-request) random token, embed it in your forms/headers, and require the server to verify it on every state-changing request. Because the attacker's cross-origin page cannot read your token (the same-origin policy hides the response), they cannot forge a valid request. The synchroniser-token and double-submit-cookie patterns both implement this. Pair tokens with SameSite for layered protection.
Insecure vs secure
app.post("/account/email", (req, res) => {
// Authorised only by the session cookie
const userId = req.session.userId;
updateEmail(userId, req.body.email);
res.redirect("/account");
});Any cross-site page can auto-submit a form to this endpoint; the browser attaches the session cookie, so the forged email change succeeds.
app.post("/account/email", verifyCsrfToken, (req, res) => {
const userId = req.session.userId;
updateEmail(userId, req.body.email);
res.redirect("/account");
});
// session cookie set with SameSite=Lax + a CSRF token
// embedded in the form and checked by verifyCsrfTokenThe request must carry a valid anti-CSRF token the attacker cannot read cross-origin, and SameSite=Lax blocks the cross-site POST as a second layer.
Key takeaways
- CSRF abuses the browser's automatic attachment of cookies to forge requests.
- Never change state on a GET request.
- SameSite=Lax/Strict cookies block the classic cross-site form attack.
- Anti-CSRF tokens prove the request came from your own page.
- Combine SameSite cookies and tokens for defence in depth.
Knowledge check
Practice
Apply this lesson hands-on in the matching find-it / fix-it challenge.
Open challenge →