Synchronizer Token
The synchronizer-token pattern stops CSRF by binding requests to a server-issued, per-session token. The server issues it; the client echoes it in a header or hidden field; the server verifies on every state-changing request.
Express + Laravel + manual middleware
EXAMPLE
// 1) Express — issue + verify on /api routes
import crypto from 'crypto';
import session from 'express-session';
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: { httpOnly: true, secure: true, sameSite: 'lax' },
}));
// Generate per-session token, expose via /csrf
app.get('/csrf', (req, res) => {
req.session.csrf ??= crypto.randomBytes(32).toString('base64url');
res.json({ token: req.session.csrf });
});
// Middleware — require valid token on mutating methods
app.use((req, res, next) => {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const sent = req.headers['x-csrf-token'] ?? req.body?.csrf;
if (!sent || !req.session.csrf || !crypto.timingSafeEqual(
Buffer.from(sent),
Buffer.from(req.session.csrf),
)) {
return res.status(403).json({ error: 'csrf' });
}
next();
});
// Client (fetch) — bootstrap the token, send it on writes
const { token } = await fetch('/csrf', { credentials: 'include' }).then(r => r.json());
await fetch('/api/posts', {
method: 'POST',
credentials: 'include',
headers: { 'content-type': 'application/json', 'x-csrf-token': token },
body: JSON.stringify({ title: 'Hi' }),
});
// 2) Double-submit cookie pattern — stateless variant
app.use((req, res, next) => {
if (!req.cookies['csrf']) {
const t = crypto.randomBytes(32).toString('base64url');
res.cookie('csrf', t, { sameSite: 'lax', secure: true });
}
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
if (req.cookies['csrf'] !== req.headers['x-csrf-token']) {
return res.status(403).json({ error: 'csrf' });
}
next();
});
# 3) Laravel — CSRF is built in
# routes/web.php uses the VerifyCsrfToken middleware automatically.
# Blade form:
# <form method="POST" action="/posts">
# @csrf
# ...
# </form>
# Or in JS:
# const token = document.querySelector('meta[name="csrf-token"]').content;
# fetch('/posts', { method: 'POST', headers: { 'X-CSRF-TOKEN': token } });
# Layout:
# <meta name="csrf-token" content="{{ csrf_token() }}">
# 4) Django — also built in
# {% csrf_token %} {# template tag in forms #}
# CsrfViewMiddleware {# already in MIDDLEWARE #}
# Cookie: csrftoken=... {# read in JS, send in header #}
# Header: X-CSRFToken: <value>
# 5) Rotating the token
# • After login/logout — invalidate the old session token
# • If risk-based — on suspicious activity, re-issue
# • Always use a single token per session — not per request (UX)
# 6) Don't do these
# - Tokens in URLs (leak via Referer + logs)
# - Plain-text comparison (use timingSafeEqual)
# - GET-with-side-effects (always require POST/PUT/DELETE for state changes)
# - 'csrftoken' in localStorage (XSS reads it; SameSite + httpOnly cookie wins)
# 7) Defence-in-depth — combine with SameSite
# SameSite=Lax on the session cookie kills cross-site POSTs by default.
# CSRF tokens then cover the remaining edge cases (subdomain trust, legacy clients).
Why it matters
Synchronizer tokens + SameSite=Lax cookies + strict CORS are the modern triple-defence. Each layer covers what the others miss; bypassing all three at once requires a separate XSS vulnerability.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Server issues a random token per session.
<form method="post" action="/transfer">
<input type="hidden" name="csrf" value="{{ csrf_token }}">
…
</form>
// Server
if (req.body.csrf !== req.session.csrf) return res.status(403).end();
Try it Yourself »
Exercise
Token placement in a classic form.
<input type="
" name="csrf" value="…">
Six letters.
Discussion
Loading…