Cross-site request forgery happens when another site causes a logged-in user's browser to send a request to your app, and the browser attaches the session cookie automatically. Without a defense, that forged request runs with the user's privileges. The fix is two layered pieces: set sameSite on your session cookie, and require an unguessable CSRF token on state-changing requests.
Why it's a problem
Because the browser sends your cookie on any request to your domain, an attacker does not need the user's credentials, only a page the user visits while logged in. A hidden form or image request can change an email, transfer a balance, or delete data as the victim. Any cookie-authenticated POST, PUT, PATCH, or DELETE without a token is a candidate.
The pattern
// cookie session, no token check: a cross-site form can trigger this
app.post("/account/email", requireLogin, (req, res) => {
updateEmail(req.user.id, req.body.email);
res.json({ ok: true });
});The fix
import csrf from "csurf";
const csrfProtection = csrf({ cookie: false });
// session cookie should also be sameSite: 'lax' or 'strict'
app.post("/account/email", requireLogin, csrfProtection, (req, res) => {
// request is rejected unless it carries the token issued to this session
updateEmail(req.user.id, req.body.email);
res.json({ ok: true });
});Why AI tools write this
CSRF is invisible in the happy path an assistant is building: the form works, the update succeeds, nothing looks wrong. The defense requires wiring a token through the template and the request, which adds steps that are not needed to make the feature function, so the assistant ships the working route without it.
The quick fix
- Set sameSite: 'lax' or 'strict' on the session cookie as the first layer.
- Require a per-session CSRF token on every state-changing request and verify it server-side.
- Prefer token-in-header for APIs called by your own frontend.
- Exempt only endpoints that use a non-cookie auth scheme like a bearer token.