/** * CSRF Protection Middleware (Modern Approach) * * Uses SameSite cookies + double-submit cookie pattern * Replaces deprecated csurf package * * Reference: OWASP CSRF Prevention Cheat Sheet * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html */ const crypto = require('crypto'); const { logSecurityEvent, getClientIp } = require('../utils/security-logger'); /** * Generate CSRF token */ function generateCsrfToken() { return crypto.randomBytes(32).toString('hex'); } /** * CSRF Protection Middleware * * Uses double-submit cookie pattern: * 1. Server sets CSRF token in secure, SameSite cookie * 2. Client must send same token in custom header (X-CSRF-Token) * 3. Server validates cookie matches header */ function csrfProtection(req, res, next) { // Skip GET, HEAD, OPTIONS (safe methods) if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); } // Get CSRF token from cookie const cookieToken = req.cookies['csrf-token']; // Get CSRF token from header const headerToken = req.headers['x-csrf-token'] || req.headers['csrf-token']; // Validate tokens exist and match if (!cookieToken || !headerToken || cookieToken !== headerToken) { logSecurityEvent({ type: 'csrf_violation', sourceIp: getClientIp(req), userId: req.user?.id, endpoint: req.path, userAgent: req.get('user-agent'), details: { method: req.method, hasCookie: !!cookieToken, hasHeader: !!headerToken, tokensMatch: cookieToken === headerToken }, action: 'blocked', severity: 'high' }); return res.status(403).json({ error: 'Forbidden', message: 'Invalid CSRF token', code: 'CSRF_VALIDATION_FAILED' }); } next(); } /** * Middleware to set CSRF token cookie * Apply this globally or on routes that need CSRF protection */ function setCsrfToken(req, res, next) { // Only set cookie if it doesn't exist if (!req.cookies['csrf-token']) { const token = generateCsrfToken(); //Check if we're behind a proxy (X-Forwarded-Proto header) const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; res.cookie('csrf-token', token, { httpOnly: false, // Must be false for double-submit pattern (client needs to read it) secure: isSecure && process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 24 * 60 * 60 * 1000 // 24 hours }); } next(); } /** * Endpoint to get CSRF token for client-side usage * GET /api/csrf-token * * Returns the CSRF token from the cookie (or creates one if missing) * This is required for pages served as static HTML by nginx (bypassing setCsrfToken middleware) */ function getCsrfToken(req, res) { let token = req.cookies['csrf-token']; // If no token exists, create one (for static HTML pages served by nginx) if (!token) { token = generateCsrfToken(); // Check if we're behind a proxy (X-Forwarded-Proto header) const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; res.cookie('csrf-token', token, { httpOnly: false, // Must be false for double-submit pattern (client needs to read it) secure: isSecure && process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 24 * 60 * 60 * 1000 // 24 hours }); } res.json({ csrfToken: token }); } module.exports = { csrfProtection, setCsrfToken, getCsrfToken, generateCsrfToken };