const helmet = require('helmet'); /** * Helmet security configuration * Helmet helps secure Express apps by setting various HTTP headers */ const helmetConfig = helmet({ // Content Security Policy contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for Swagger UI scriptSrc: ["'self'", "'unsafe-inline'"], // Allow inline scripts for Swagger UI imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'", "data:"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"] } }, // Cross-Origin-Embedder-Policy crossOriginEmbedderPolicy: false, // Disabled for API compatibility // Cross-Origin-Opener-Policy crossOriginOpenerPolicy: { policy: "same-origin" }, // Cross-Origin-Resource-Policy crossOriginResourcePolicy: { policy: "cross-origin" }, // DNS Prefetch Control dnsPrefetchControl: { allow: false }, // Expect-CT (deprecated but included for older browsers) expectCt: { maxAge: 86400 }, // Frameguard (prevent clickjacking) frameguard: { action: "deny" }, // Hide Powered-By header hidePoweredBy: true, // HTTP Strict Transport Security hsts: { maxAge: 31536000, // 1 year includeSubDomains: true, preload: true }, // IE No Open ieNoOpen: true, // No Sniff (prevent MIME type sniffing) noSniff: true, // Origin-Agent-Cluster originAgentCluster: true, // Permitted Cross-Domain Policies permittedCrossDomainPolicies: { permittedPolicies: "none" }, // Referrer Policy referrerPolicy: { policy: "no-referrer" } }); /** * Custom security headers middleware * Only adds headers not already set by Helmet */ const customSecurityHeaders = (req, res, next) => { // Add Permissions-Policy (not in Helmet) res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); // Prevent caching of sensitive data if (req.path.includes('/api/auth') || req.path.includes('/api/admin') || req.path.includes('/api/users')) { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.setHeader('Surrogate-Control', 'no-store'); } next(); }; /** * CORS configuration */ const getCorsOptions = () => { const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : ['http://localhost:3000', 'http://localhost:4200', 'http://localhost:5173']; return { origin: (origin, callback) => { // Allow requests with no origin (mobile apps, Postman, etc.) if (!origin) return callback(null, true); if (allowedOrigins.indexOf(origin) !== -1 || process.env.NODE_ENV === 'development') { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'x-guest-token'], exposedHeaders: ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset'], maxAge: 86400 // 24 hours }; }; /** * Security middleware for API routes */ const secureApiRoutes = (req, res, next) => { // Log security-sensitive operations if (req.method !== 'GET' && req.path.includes('/api/admin')) { const logger = require('../config/logger'); logger.logSecurityEvent(`Admin ${req.method} request`, req); } next(); }; /** * Prevent parameter pollution * This middleware should be used after body parser */ const preventParameterPollution = (req, res, next) => { // Whitelist of parameters that can have multiple values const whitelist = ['category', 'difficulty', 'tags', 'keywords']; // Check for duplicate parameters if (req.query) { for (const param in req.query) { if (Array.isArray(req.query[param]) && !whitelist.includes(param)) { return res.status(400).json({ status: 'error', message: `Parameter pollution detected: '${param}' should not have multiple values` }); } } } next(); }; module.exports = { helmetConfig, customSecurityHeaders, getCorsOptions, secureApiRoutes, preventParameterPollution };