A state-changing request (change email, transfer funds, add a user) is accepted on the strength of the browser's cookies alone. A hostile page the victim visits while signed in can submit that request, and the application cannot tell it apart from a real one.
At a glance
- Why it matters
- The attacker performs actions as the victim without seeing the response: changing the account email to take the account over, moving money, or granting access. High-privilege users are the usual target because one forged request from an admin session changes the whole application.
- How to fix it
- Use SameSite=Lax or Strict session cookies and a synchronizer or double-submit token on every state-changing request. Never change state on GET. For APIs used by browsers, require a custom header or a JSON content type that a cross-site form cannot send.
Description
CSRF tricks a victim's browser into submitting a request to an application the victim is already authenticated to, without the victim intending or knowing it happened. Because browsers attach cookies automatically to any request sent to the matching domain, a malicious page can trigger a state-changing request (a form auto-submitted by hidden JavaScript, or even a simple image tag pointed at a GET endpoint that changes state) and the target application sees a request that looks legitimate: it carries a valid session cookie, it just was not something the user meant to send.
Impact
CSRF is limited to actions the victim's account is itself permitted to perform, but that is often enough: changing an email address or password (which can be a path to full account takeover once the attacker controls the reset flow), transferring funds, changing security settings, or submitting data on the victim's behalf. It is a particularly effective attack against high-privilege accounts, since it inherits whatever access the logged-in victim already has.
How we find it
- We test every state-changing endpoint (anything that is not a plain read) for a CSRF token, and then test whether that token is actually validated, not merely present: removing it, reusing an old one, or submitting another user's valid token against the wrong session.
- We check the
SameSitecookie attribute on session cookies, sinceSameSite=LaxorStrictcloses much of this class on its own for modern browsers, and its absence (orSameSite=Nonewithout a compensating token check) is itself worth flagging. - We build a minimal proof-of-concept page that auto-submits a cross-origin request to the target endpoint from an authenticated test session, to confirm the request succeeds in practice, not just in theory.
Fix
Require a per-session (or per-request) CSRF token on every state-changing request, verified server-side against the session it was issued for, and set SameSite=Lax or Strict on session cookies as a second, independent layer. Never rely on a GET request to perform a state change: GET requests are exactly what an attacker can trigger with nothing more than an image tag.
// Server: issue a per-session token and require it on state-changing requests
app.post("/account/email", requireCsrfToken, (req, res) => {
// requireCsrfToken compares req.body.csrfToken (or a header) against
// the token bound to req.session, rejecting the request if it differs
updateEmail(req.session.userId, req.body.email);
res.sendStatus(204);
});
// Cookie: SameSite is a second, independent layer, not a replacement
res.cookie("session", sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax",
});
References
- OWASP Top 10: A01:2021, Broken Access Control (CSRF is grouped here since 2021)
- CWE-352: Cross-Site Request Forgery
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet