CSRF Protection

Cookie-based auth needs CSRF defense; SameSite cookies and the double-submit token are your two layers — and header-token APIs mostly don't.

Cross-Site Request Forgery tricks a logged-in user's browser into making a state-changing request to your site. It only works because browsers automatically attach your cookies to any request to your domain, even one triggered by an attacker's page. Understand that and the defenses follow.

Who needs to worry

CSRF targets cookie-based auth. A pure API that authenticates with an Authorization: Bearer header is largely immune, because the attacker's page cannot read your token or set that header cross-origin. Sessions and auth cookies, however, need protection.

Two layers, use both

  • SameSite cookies — set sameSite: 'lax' (or 'strict'). The browser then withholds the cookie on most cross-site requests. This alone stops the majority of CSRF.
  • Anti-CSRF token — the double-submit pattern: issue a random token in a readable cookie and require it echoed back in a header. An attacker's cross-origin page cannot read the cookie, so it cannot forge the header.

Example

Example · javascript
import express from 'express';
import cookieParser from 'cookie-parser';
import { doubleCsrf } from 'csrf-csrf';

const app = express();
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(express.json());

const { generateToken, doubleCsrfProtection } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: '__Host-csrf',
  cookieOptions: { sameSite: 'lax', secure: true, path: '/' },
  // Only guard state-changing methods; GET/HEAD/OPTIONS are safe.
  ignoredMethods: ['GET', 'HEAD', 'OPTIONS'],
});

// The SPA fetches a token first, then echoes it in the x-csrf-token header.
app.get('/csrf-token', (req, res) => {
  res.json({ token: generateToken(req, res) });
});

// Protection verifies cookie-vs-header match on mutating requests.
app.post('/api/transfer', doubleCsrfProtection, (req, res) => {
  res.json({ ok: true, moved: req.body.amount });
});

When to use it

  • A server-rendered Express app uses csurf to generate a CSRF token embedded in every HTML form, rejecting state-changing requests without a valid token.
  • A developer disables CSRF protection for stateless JWT-authenticated API routes while keeping it active for session-based browser routes.
  • A SPA reads a CSRF token from a cookie using the double-submit cookie pattern and sends it back in a custom X-CSRF-Token header.

More examples

CSRF token in HTML form

Registers csurf with cookie storage, generates a token for the GET form route, and validates it automatically on POST.

Example · js
const csrf = require('csurf');
const csrfProtect = csrf({ cookie: true });

app.use(require('cookie-parser')());

app.get('/form', csrfProtect, (req, res) => {
  // req.csrfToken() generates and stores the token
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/submit', csrfProtect, (req, res) => {
  // csurf validates _csrf field from req.body automatically
  res.json({ ok: true });
});

CSRF error handler

Catches the EBADCSRFTOKEN error code thrown by csurf and returns a 403 Forbidden response with a clear message.

Example · js
app.use((err, req, res, next) => {
  if (err.code === 'EBADCSRFTOKEN') {
    return res.status(403).json({ error: 'Invalid or missing CSRF token' });
  }
  next(err);
});

Double-submit cookie for SPAs

Implements the double-submit cookie pattern: the SPA reads the csrf-token cookie and echoes it in an X-CSRF-Token header.

Example · js
const crypto = require('crypto');

// Send token as a readable cookie (SPA reads and re-sends it as a header)
app.use((req, res, next) => {
  if (!req.cookies['csrf-token']) {
    res.cookie('csrf-token', crypto.randomBytes(32).toString('hex'), { sameSite: 'strict' });
  }
  next();
});

app.post('/api/action', (req, res) => {
  if (req.cookies['csrf-token'] !== req.headers['x-csrf-token']) {
    return res.status(403).json({ error: 'CSRF check failed' });
  }
  res.json({ ok: true });
});

Discussion

  • Be the first to comment on this lesson.