/** * CSRF Protection Module * Implements double-submit cookie pattern for stateless CSRF protection */ const crypto = require('crypto'); const CSRF_TOKEN_LENGTH = 32; const CSRF_COOKIE_NAME = 'dashcaddy_csrf'; const CSRF_HEADER_NAME = 'x-csrf-token'; /** * Generate a cryptographically secure CSRF token * @returns {string} Base64URL-encoded random token */ function generateToken() { return crypto.randomBytes(CSRF_TOKEN_LENGTH).toString('base64url'); } /** * Parse cookie header string into object * @param {string} cookieHeader - Cookie header value * @returns {Object} Parsed cookies */ function parseCookie(cookieHeader) { if (!cookieHeader) return {}; return cookieHeader.split(';').reduce((cookies, cookie) => { const [name, ...rest] = cookie.trim().split('='); if (name && rest.length > 0) { cookies[name] = rest.join('='); } return cookies; }, {}); } /** * Middleware to set CSRF cookie on all requests * Generates and sets a new token if none exists */ function csrfCookieMiddleware(req, res, next) { const cookies = parseCookie(req.headers.cookie); let csrfToken = cookies[CSRF_COOKIE_NAME]; // Generate new token if none exists if (!csrfToken) { csrfToken = generateToken(); } // Store token on request so endpoints can access it req.csrfToken = csrfToken; // Set cookie (SameSite=Strict for additional protection) res.cookie(CSRF_COOKIE_NAME, csrfToken, { httpOnly: false, // Must be readable by JavaScript for sending in headers secure: false, // Set to true in production with HTTPS sameSite: 'strict', path: '/', maxAge: 24 * 60 * 60 * 1000 // 24 hours }); next(); } /** * Middleware to validate CSRF token on state-changing requests * Validates that the token in the cookie matches the token in the header */ function csrfValidationMiddleware(req, res, next) { const method = req.method.toUpperCase(); // Skip validation for safe methods if (['GET', 'HEAD', 'OPTIONS'].includes(method)) { return next(); } // Skip CSRF validation in test environment if (process.env.NODE_ENV === 'test') { return next(); } // Excluded paths that don't require CSRF validation const excludedPaths = [ '/api/totp/verify', '/api/totp/verify-setup', '/health', '/api/health' ]; // Check if path starts with excluded prefix const isExcluded = excludedPaths.some(path => req.path === path) || req.path.startsWith('/api/auth/gate/'); if (isExcluded) { return next(); } // Get token from cookie const cookies = parseCookie(req.headers.cookie); const cookieToken = cookies[CSRF_COOKIE_NAME]; // Get token from header (case-insensitive) const headerToken = req.headers[CSRF_HEADER_NAME] || req.headers[CSRF_HEADER_NAME.toLowerCase()]; // Validate both tokens exist if (!cookieToken) { console.warn(`[CSRF] Missing CSRF cookie: ${method} ${req.path} from ${req.ip}`); return res.status(403).json({ success: false, error: '[DC-100] CSRF token missing', message: 'CSRF cookie not found. Please refresh the page (Ctrl+Shift+R) and try again.' }); } if (!headerToken) { console.warn(`[CSRF] Missing CSRF header: ${method} ${req.path} from ${req.ip}`); return res.status(403).json({ success: false, error: '[DC-100] CSRF token missing', message: 'CSRF token not provided in request headers. Please refresh the page (Ctrl+Shift+R) and try again.' }); } // Validate tokens match using constant-time comparison try { const cookieBuffer = Buffer.from(cookieToken, 'base64url'); const headerBuffer = Buffer.from(headerToken, 'base64url'); // Ensure buffers are same length if (cookieBuffer.length !== headerBuffer.length) { throw new Error('Token length mismatch'); } // Constant-time comparison if (!crypto.timingSafeEqual(cookieBuffer, headerBuffer)) { throw new Error('Token mismatch'); } // Tokens match - request is valid next(); } catch (err) { console.warn(`[CSRF] Invalid CSRF token: ${method} ${req.path} from ${req.ip} - ${err.message}`); return res.status(403).json({ success: false, error: '[DC-101] CSRF token invalid', message: 'CSRF token validation failed. Please refresh the page (Ctrl+Shift+R) and try again.' }); } } module.exports = { CSRF_TOKEN_LENGTH, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, generateToken, parseCookie, csrfCookieMiddleware, csrfValidationMiddleware };