/** * Content Security Policy middleware * Generates nonces for inline scripts and sets appropriate CSP headers */ const crypto = require('crypto'); const winston = require('winston'); // Security headers middleware (renamed from CSP middleware as it now handles more headers) function securityMiddleware(config) { // If security is entirely disabled, return a no-op middleware if (config.security === false) { return function(req, res, next) { next(); }; } // Get CSP configuration - default to enabled if not specified const cspEnabled = config.security && typeof config.security.csp !== 'undefined' ? config.security.csp : true; // Log the CSP configuration at startup for debugging winston.info(`Security middleware initialized with CSP ${cspEnabled ? 'ENABLED' : 'DISABLED'}`); return function(req, res, next) { // Only add security headers for HTML requests const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/); if (isHtmlRequest) { // Add basic security headers - always applied regardless of CSP setting // 1. X-Content-Type-Options - prevents MIME-type sniffing res.setHeader('X-Content-Type-Options', 'nosniff'); // 2. X-Frame-Options - prevents clickjacking res.setHeader('X-Frame-Options', 'DENY'); // 3. X-XSS-Protection - legacy header, still used by some browsers res.setHeader('X-XSS-Protection', '1; mode=block'); // 4. Referrer-Policy - controls how much referrer information is included res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); // 5. Permissions-Policy - controls browser features res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()'); // Apply CSP headers ONLY if explicitly enabled if (cspEnabled === true) { // Generate a unique nonce for this request const nonce = crypto.randomBytes(16).toString('base64'); // Store nonce in request object for use in HTML template req.cspNonce = nonce; // Build the base script sources list const scriptSources = ["'self'", `'nonce-${nonce}'`]; // Add any additional script sources from config if (config.security && config.security.scriptSources && config.security.scriptSources.length > 0) { config.security.scriptSources.forEach(source => { if (source && !scriptSources.includes(source)) { scriptSources.push(source); } }); } // Add unsafe-hashes if configured (for event handlers) if (config.security && config.security.allowUnsafeHashes) { scriptSources.push("'unsafe-hashes'"); } // Create the policy - adjust based on environment let cspDirectives; // Development mode with potential bypass option if (process.env.NODE_ENV === 'development') { // Check if we should bypass strict CSP in development const bypassCSPInDev = config.security && config.security.bypassCSPInDev; if (bypassCSPInDev) { // Very permissive policy with unsafe-inline for development testing cspDirectives = [ "default-src 'self'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data:", "connect-src 'self'", "font-src 'self'" ]; winston.debug('Using permissive development CSP policy with unsafe-inline (bypass enabled)'); } else { // Standard development mode - still using nonces cspDirectives = [ "default-src 'self'", `script-src ${scriptSources.join(' ')}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data:", "connect-src 'self'", "font-src 'self'" ]; winston.debug('Using development CSP policy with nonces (bypass disabled)'); } } else { // Production mode - strict policy with nonces cspDirectives = [ "default-src 'self'", `script-src ${scriptSources.join(' ')}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data:", "connect-src 'self'", "font-src 'self'", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'none'", "object-src 'none'" ]; winston.debug(`Using strict production CSP policy with nonces${config.security.allowUnsafeHashes ? " and unsafe-hashes" : ""}`); } // Set the CSP header with the properly formatted policy res.setHeader('Content-Security-Policy', cspDirectives.join('; ')); winston.debug('CSP header applied'); } else { winston.debug('CSP is disabled by configuration - not applying CSP header'); } // Add Cross-Origin headers only if enabled in config // These can be problematic for some applications, so we make them optional const enableCrossOriginIsolation = config.security && config.security.enableCrossOriginIsolation; if (enableCrossOriginIsolation) { // 6. Cross-Origin-Embedder-Policy - for cross-origin isolation res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); // 7. Cross-Origin-Resource-Policy - protects resources from unauthorized requests res.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); // 8. Cross-Origin-Opener-Policy - helps with cross-origin isolation res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); winston.debug('Added Cross-Origin isolation headers'); } // If configured, add HSTS header if (config.security && config.security.hsts) { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); } } next(); }; } // Export the middleware module.exports = securityMiddleware;