import helmet from 'helmet'; import cors from 'cors'; import hpp from 'hpp'; import mongoSanitize from 'express-mongo-sanitize'; import { logger } from '../utils/logger.js'; import { config } from '../config/index.js'; import crypto from 'crypto'; /** * Configure CORS */ export const corsOptions = { origin: function (origin, callback) { const allowedOrigins = config.cors?.allowedOrigins || [ 'http://localhost:8080', 'http://localhost:3000', 'https://app.marketing-agent.com' ]; // Allow requests with no origin (like mobile apps or Postman) if (!origin) return callback(null, true); if (allowedOrigins.indexOf('*') !== -1 || allowedOrigins.indexOf(origin) !== -1) { callback(null, true); } else { logger.warn('CORS blocked request', { origin, ip: origin }); callback(new Error('Not allowed by CORS')); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID'], exposedHeaders: ['X-Request-ID', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'], maxAge: 86400 // 24 hours }; /** * Configure Helmet security headers */ export const helmetConfig = helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], upgradeInsecureRequests: config.environment === 'production' ? [] : null } }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } }); /** * Request ID middleware */ export function requestId(req, res, next) { const id = req.get('X-Request-ID') || crypto.randomUUID(); req.id = id; res.setHeader('X-Request-ID', id); next(); } /** * IP whitelist/blacklist middleware */ export function ipFilter(options = {}) { const whitelist = options.whitelist || []; const blacklist = options.blacklist || []; return (req, res, next) => { const clientIp = req.ip || req.connection.remoteAddress; // Check blacklist first if (blacklist.length > 0 && blacklist.includes(clientIp)) { logger.warn('Blacklisted IP attempted access', { ip: clientIp }); return res.status(403).json({ success: false, error: 'Access denied' }); } // Check whitelist if configured if (whitelist.length > 0 && !whitelist.includes(clientIp)) { logger.warn('Non-whitelisted IP attempted access', { ip: clientIp }); return res.status(403).json({ success: false, error: 'Access denied' }); } next(); }; } /** * Security headers middleware */ export function securityHeaders(req, res, next) { // Additional security headers res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); // Remove potentially sensitive headers res.removeHeader('X-Powered-By'); res.removeHeader('Server'); next(); } /** * API version check middleware */ export function apiVersionCheck(supportedVersions = ['v1']) { return (req, res, next) => { const version = req.headers['api-version'] || req.query.apiVersion; if (version && !supportedVersions.includes(version)) { return res.status(400).json({ success: false, error: 'Unsupported API version', supportedVersions }); } req.apiVersion = version || supportedVersions[0]; next(); }; } /** * Prevent parameter pollution */ export const preventParamPollution = hpp({ whitelist: ['sort', 'fields', 'filter', 'page', 'limit'] }); /** * MongoDB injection prevention */ export const preventMongoInjection = mongoSanitize({ replaceWith: '_', onSanitize: ({ req, key }) => { logger.warn('MongoDB injection attempt prevented', { ip: req.ip, path: req.path, key }); } }); /** * Request logging middleware */ export function requestLogger(req, res, next) { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.info('Request processed', { requestId: req.id, method: req.method, path: req.path, statusCode: res.statusCode, duration, ip: req.ip, userAgent: req.get('user-agent'), userId: req.user?.id }); }); next(); } /** * Error logging middleware */ export function errorLogger(err, req, res, next) { logger.error('Request error', { requestId: req.id, error: err.message, stack: err.stack, method: req.method, path: req.path, ip: req.ip, userId: req.user?.id }); next(err); } /** * Security audit middleware */ export function securityAudit(eventType) { return (req, res, next) => { logger.info('Security event', { eventType, requestId: req.id, userId: req.user?.id, ip: req.ip, path: req.path, method: req.method, timestamp: new Date().toISOString() }); next(); }; } /** * Trusted proxy configuration */ export function configureTrustedProxies(app) { // Trust proxies for accurate IP detection app.set('trust proxy', config.trustProxy || ['loopback', 'linklocal', 'uniquelocal']); } /** * Session fixation prevention */ export function preventSessionFixation(req, res, next) { if (req.session && req.user) { // Regenerate session ID on login if (req.path.includes('/login') && res.statusCode === 200) { req.session.regenerate((err) => { if (err) { logger.error('Session regeneration failed', { error: err }); } next(); }); return; } } next(); } /** * CSRF protection for state-changing operations */ export function csrfProtection(options = {}) { const excludePaths = options.exclude || ['/api/v1/auth/login', '/api/v1/auth/register']; return (req, res, next) => { // Skip for excluded paths if (excludePaths.some(path => req.path.includes(path))) { return next(); } // Skip for safe methods if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); } const token = req.headers['x-csrf-token'] || req.body._csrf; const sessionToken = req.session?.csrfToken; if (!token || !sessionToken || token !== sessionToken) { return res.status(403).json({ success: false, error: 'Invalid CSRF token' }); } next(); }; } /** * Apply all security middleware */ export function applySecurityMiddleware(app) { // Basic security app.use(helmetConfig); app.use(cors(corsOptions)); app.use(requestId); app.use(securityHeaders); // Request processing app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Injection prevention app.use(preventMongoInjection); app.use(preventParamPollution); // Logging app.use(requestLogger); // Configure trusted proxies configureTrustedProxies(app); }