Cross-Site Request Forgery (CSRF)
Also known as: CSRF, XSRF, Session riding, One-click attack
A web attack in which a page on another site causes the victim's browser to send a state-changing request to an application where they are signed in, and the browser attaches their session cookie automatically.
How it works
Cross-site request forgery abuses the trust a web application places in a signed-in browser. When a request reaches the server carrying a valid session cookie, the application assumes the user meant to send it. CSRF breaks that assumption: a request can be started by a different site, yet the browser still attaches the cookie for the target site.
The flaw needs three conditions. The action changes state, such as changing an email address or moving money. The session is carried only by a cookie that the browser sends on cross-site requests. And nothing in the request proves the user's intent, such as a secret value the other site cannot read. The attacker never sees the response, so the harm is the action itself, not data theft.
The defence is to require something a foreign site cannot supply. A per-session or per-request token that the server validates, a cookie marked SameSite so the browser withholds it from cross-site posts, and checks of the Origin or Referer header each raise the bar. Sensitive actions such as password or email change should also ask for the current password or a fresh authentication.
These controls are layered on purpose. Tokens are the primary defence, SameSite is a strong default that reduces exposure, and origin checks catch what is left. Safe methods such as GET must never change state, and cross-origin policy settings such as permissive CORS must not undo the protection.
Walk through it
- 1Review the endpoint
- 2Name the missing proof
- 3Check the cookie policy
- Add the token check
- Harden and verify the fix
A pre-release review flags the account settings page. It lets a signed-in user change the email used for password recovery. Read the handler as a reviewer and note what it trusts to decide the request is genuine.
1// POST /account/email (state-changing)2router.post("/account/email", requireSession, async (req, res) => {3 const user = req.session.user;4 await db.users.update(user.id, { email: req.body.email });5 res.redirect("/account?updated=1");6});Spot it
- State-changing requests that arrive with a valid session but no anti-CSRF token, or a token that fails validation.
- Requests whose Origin or Referer header names a different site than the application.
- Sensitive actions, such as an email or payout change, immediately preceded by a page view on an unrelated domain.
- Account changes the user later says they did not make, with no matching interaction in the application's own pages.
- Missing or None SameSite attribute on session cookies in a configuration review or scan.
Application access log
ts=2026-10-11T09:14:02Z method=POST path=/account/email user=u_4821 status=302 origin=https://promo-offers.example referer=https://promo-offers.example/win csrf_token=absent
ts=2026-10-11T09:14:40Z method=POST path=/account/email user=u_4821 status=302 origin=https://portal.fabrikam-bank.example csrf_token=validWAF
ts=2026-10-11T09:14:02Z rule=cross-origin-state-change action=log host=portal.fabrikam-bank.example path=/account/email origin_mismatch=truesplSplunk: state-changing requests from a foreign origin
index=app sourcetype=access method IN (POST,PUT,PATCH,DELETE)
| where isnotnull(origin) AND NOT match(origin, "^https://portal\.fabrikam-bank\.example$")
| stats count by user, path, originExpect some legitimate partner integrations; allow-list known origins before alerting.
grepgrep: unprotected state-changing requests in access logs
grep -E 'method=(POST|PUT|PATCH|DELETE)' access.log | grep 'csrf_token=absent'Stop it
Validate an anti-CSRF token on every state-changing request
Issue an unpredictable token tied to the user's session, embed it in your own forms or send it in a custom header, and have the server reject any state-changing request where it is missing or wrong. A foreign site cannot read the token, so it cannot include it.
Set SameSite on session cookies
Mark cookies SameSite=Lax at minimum, or Strict where the flow allows, so the browser withholds them from cross-site form posts. Treat this as defence in depth, not a replacement for tokens.
Verify origin and require re-authentication for sensitive actions
Check that the Origin or Referer header matches your own site, never change state on GET, and ask for the current password or a fresh sign-in before changing credentials, email or payment details.
Reject state-changing requests that lack a valid token
Vulnerable
router.post("/account/email", requireSession, async (req, res) => {
await db.users.update(req.session.user.id, { email: req.body.email });
res.redirect("/account?updated=1");
});Hardened
router.post("/account/email", requireSession, verifyCsrfToken, requireRecentAuth, async (req, res) => {
await db.users.update(req.session.user.id, { email: req.body.email });
res.redirect("/account?updated=1");
});
function verifyCsrfToken(req, res, next) {
const sent = req.get("x-csrf-token") ?? req.body._csrf;
if (!sent || !timingSafeEqual(sent, req.session.csrfToken)) return res.status(403).end();
next();
}Use your framework's built-in CSRF middleware where one exists; the sketch shows the shape of the check.
Session cookie attributes
Hardened
Set-Cookie: session=<opaque>; Path=/; Secure; HttpOnly; SameSite=Lax- Use the CSRF protection built into your framework and enable it by default for every non-safe method.
- Set SameSite=Lax or Strict on session cookies and keep Secure and HttpOnly on.
- Never change state on GET, HEAD or OPTIONS requests.
- Validate the Origin header on state-changing requests and reject mismatches.
- Require the current password or fresh authentication for email, password and payment changes.
- Keep CORS allow-lists tight and never combine wildcard origins with credentialed requests.
If it already happened
Ship a server-side token check or temporarily disable the affected action, and invalidate recovery emails or settings changed in the suspect window.
Identify accounts changed without a matching in-application session, restore their email, credentials and payout details, and reset sessions.
Notify affected users, confirm the fix with a test that posts without a token and from a foreign origin, and monitor the endpoint for rejected requests.
Make CSRF protection a default in the framework template, add a review checklist item for every new state-changing route, and add the log detection above.
Check yourself
1. Why can a page on another site trigger an action on an application where the user is signed in?
2. Which control is the primary defence against CSRF?
3. What does SameSite=Lax on a session cookie achieve?