Stop Forged Cross-Site Requests
CSRF Protection
CSRF lets an attacker trick a user's browser into making requests on their behalf. SameSite + tokens stops it.
What you'll learn
- Understand CSRF
- Use SameSite cookies as the first layer
- Add CSRF tokens for sensitive endpoints
CSRF (Cross-Site Request Forgery): an attacker tricks a logged-in user’s browser into making a request to your site. The browser includes the user’s cookies. Your server sees a “legitimate” request and processes it.
The Attack
1. Alice is logged into your-bank.com (cookie set)
2. Alice visits attacker.com (which has a hidden form)
3. Attacker's page auto-submits: POST your-bank.com/transfer
4. Browser includes Alice's cookies
5. Your server transfers Alice's money
The fix is making your server distinguish “request from your site” vs “request from elsewhere.”
Layer 1 — SameSite Cookies
The 2020s default kills most CSRF:
res.cookie("session", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
}); With SameSite=Lax, the browser does not send your cookie on
a cross-site POST. The attack above fails — the bank’s server gets
the request with no session cookie.
For many apps, SameSite=Lax alone is enough.
Layer 2 — CSRF Tokens
For state-changing endpoints (POST, PUT, DELETE), require a token that lives in your page AND in a cookie.
npm install csurf (Note: csurf was deprecated. The actively maintained fork is
csrf-csrf — same idea.)
import { doubleCsrf } from "csrf-csrf";
const { doubleCsrfProtection, generateToken } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: "x-csrf-token",
cookieOptions: { httpOnly: true, sameSite: "lax", secure: true },
});
// On a page-rendering route, generate a token to embed
app.get("/dashboard", (req, res) => {
const token = generateToken(req, res);
res.render("dashboard", { csrfToken: token });
});
// Protect mutating routes
app.post("/transfer", doubleCsrfProtection, (req, res) => {
// request only passes here if the token was valid
}); The client sends the token in a header (x-csrf-token) or form
field; the middleware verifies it against the cookie.
When You DON’T Need CSRF Tokens
If your API is cookie-less (using Authorization: Bearer ...
headers), the attack vector doesn’t apply — the browser doesn’t
auto-attach the header to cross-site requests.
For pure JSON APIs with JWTs in the Authorization header, CSRF
is not a concern. (But XSS still is — keep tokens out of
JavaScript-reachable storage if possible.)
SPA + Cookie API
If your SPA uses cookies for auth (common pattern), you do need
CSRF protection. SameSite=Lax + explicit tokens for sensitive
operations covers it.
The Cookbook
| Setup | CSRF Strategy |
|---|---|
| Server-rendered app + session cookies | SameSite=Lax + token for forms |
| SPA + cookie-based API | SameSite=Lax + token for mutations |
API + JWT in Authorization header | None needed |
| Mobile app + JWT | None needed |