security
CSRF, and when a JWT setup quietly needs it anyway
CSRF, and when a JWT setup quietly needs it anyway
A malicious site tricks a logged-in user's browser into making a request to your app, and the browser automatically attaches the user's cookies — because any request to yourapp.com carries yourapp.com's cookies regardless of what page initiated it. If the app trusts "has a valid session cookie" as proof of intent, an attacker can make the browser act as the user — change an email, submit a form — without the user knowingly doing anything.
This only matters for cookie-based auth. An API strictly requiring a JWT in an Authorization header is mostly safe from it — a forged request can't attach a header value the attacker doesn't know, and same-origin policy blocks reading localStorage from another origin.
The bit that catches people
I needed CSRF protection anyway, because refresh tokens are httpOnly cookies. That's good practice — it keeps them out of reach of XSS — but it reintroduces the cookie problem: the browser sends that cookie along with a forged cross-site request too. Any endpoint reachable via a cookie-carrying request needs protection.
The standard defences, layered:
- A double-submit or synchroniser token. The server issues a CSRF token the frontend must echo in a custom header on state-changing requests, which a forged cross-site request can't read or replay.
SameSite=LaxorStricton the cookie itself, telling the browser not to send it on cross-site requests at all.Laxstill allows top-level navigation like a clicked link;Strictis tighter but can break legitimate flows like a payment redirect coming back.- Checking
Origin/Refererheaders as a lighter secondary check — not airtight alone, since some proxies and extensions strip headers, but another layer.
The CSRF work in this project's history landed alongside CI and CodeQL fixes, which isn't a coincidence — CodeQL is exactly the kind of static analysis that flags a missing CSRF check on a cookie-authenticated, state-changing endpoint.