import { COOKIE_NAMES } from '../utils/cookie-options.js'; const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); function getCookieValuesFromHeader(cookieHeader, name) { if (!cookieHeader || typeof cookieHeader !== 'string') return []; const values = []; const pattern = new RegExp(`(?:^|;\\s*)${name}=([^;]*)`, 'g'); let match; while ((match = pattern.exec(cookieHeader)) !== null) { values.push(match[1]); } return values; } export function csrfMiddleware(options = {}) { const { cookieName = COOKIE_NAMES.csrf, headerName = 'x-csrf-token', requireOriginCheck = false, allowedOrigin = null } = options; return function csrf(req, res, next) { if (SAFE_METHODS.has(req.method)) return next(); // Optional origin check hardening (recommended in production) if (requireOriginCheck && allowedOrigin) { const origin = req.headers.origin; const referer = req.headers.referer; const ok = (origin && origin === allowedOrigin) || (!origin && referer && referer.startsWith(allowedOrigin)); if (!ok) { return res.status(403).json({ error: 'CSRF origin check failed', code: 'CSRF_ORIGIN_FAILED' }); } } const csrfCookie = req.cookies?.[cookieName]; const csrfHeader = req.headers[headerName]; // Handle duplicate cookies with the same name (e.g. legacy '/api' path plus // current '/' path). cookie-parser will pick one value, but the browser may // send both. Accept if the header matches ANY provided cookie value. const rawHeader = req.headers?.cookie || ''; const rawValues = getCookieValuesFromHeader(rawHeader, cookieName).map(v => { try { return decodeURIComponent(v); } catch { return v; } }); const anyMatch = csrfHeader && rawValues.includes(csrfHeader); if (!csrfHeader || (!csrfCookie && !anyMatch) || (csrfCookie !== csrfHeader && !anyMatch)) { return res.status(403).json({ error: 'CSRF validation failed', code: 'CSRF_FAILED' }); } return next(); }; }