import crypto from 'crypto'; import logger from '../utils/logger.js'; const CSRF_COOKIE = '_csrf'; const CSRF_HEADER = 'x-csrf-token'; function generateToken() { return crypto.randomBytes(32).toString('hex'); } export function csrfMiddleware(req, res, next) { let token = req.cookies?.[CSRF_COOKIE]; if (!token) { token = generateToken(); res.cookie(CSRF_COOKIE, token, { httpOnly: false, sameSite: false, path: '/' }); } res.locals.csrfToken = token; next(); } export function validateCsrf(req, res, next) { // Only validate state-changing methods if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next(); // Skip multipart/form-data — multer hasn't parsed the body yet. // Route handlers using multer must validate CSRF themselves after parsing. const ct = req.headers['content-type'] || ''; if (ct.startsWith('multipart/form-data')) return next(); const token = req.body?._csrf || req.headers[CSRF_HEADER]; const cookieToken = req.cookies?.[CSRF_COOKIE]; if (!token || !cookieToken) { logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method, hasBodyToken: !!token, hasCookieToken: !!cookieToken, contentType: ct }, 'CSRF token missing'); return res.status(403).send('CSRF token missing. Please reload the page.'); } const provided = Buffer.from(token, 'utf8'); const expected = Buffer.from(cookieToken, 'utf8'); if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) { logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token mismatch'); return res.status(403).send('CSRF token invalid. Please reload the page.'); } next(); } /** * Validate CSRF token from req.body._csrf (for use inside multer handlers). * Returns true if valid, false otherwise. Sends 403 response on failure. */ export function validateCsrfFromBody(req, res) { const token = req.body?._csrf; const cookieToken = req.cookies?.[CSRF_COOKIE]; if (!token || !cookieToken) { logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token missing (multipart)'); res.status(403).send('CSRF token missing. Please reload the page.'); return false; } const provided = Buffer.from(token, 'utf8'); const expected = Buffer.from(cookieToken, 'utf8'); if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) { logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token mismatch (multipart)'); res.status(403).send('CSRF token invalid. Please reload the page.'); return false; } return true; }