// Copyright (c) 2026 Gordon Bolton. MIT License. /** * CSRF protection using the double-submit cookie pattern. * * On login the server issues a random `csrf_token` cookie (httpOnly:false * so JS can read it). The SPA must send the same value in the * `X-CSRF-Token` request header for every state-changing request (POST, * PUT, PATCH, DELETE). * * Because the `sameSite: strict` session cookie already provides strong * protection in modern browsers, this acts as defence-in-depth for * older browsers and any edge cases. * * Safe methods (GET, HEAD, OPTIONS) are exempted. */ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); function verifyCsrf(req, res, next) { if (SAFE_METHODS.has(req.method)) return next(); const cookieToken = req.cookies.csrf_token; const headerToken = req.headers['x-csrf-token']; if (!cookieToken || !headerToken) { return res.status(403).json({ error: 'CSRF token missing' }); } // Constant-time comparison to prevent timing attacks if (cookieToken.length !== headerToken.length) { return res.status(403).json({ error: 'CSRF token invalid' }); } const a = Buffer.from(cookieToken); const b = Buffer.from(headerToken); if (!require('crypto').timingSafeEqual(a, b)) { return res.status(403).json({ error: 'CSRF token invalid' }); } next(); } module.exports = verifyCsrf;