CSRF Protection

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.

3 min read Level 3/5 #express#csrf#security
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.)

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

SetupCSRF Strategy
Server-rendered app + session cookiesSameSite=Lax + token for forms
SPA + cookie-based APISameSite=Lax + token for mutations
API + JWT in Authorization headerNone needed
Mobile app + JWTNone needed
Rate Limiting →