/** * Comprehensive Security Middleware * Handles various security concerns and provides protection against common attacks */ const { logger } = require('./errorHandler'); // Redis client will be passed from the main app let redisClient = null; // Function to set Redis client function setRedisClient(client) { redisClient = client; } /** * Brute force protection middleware */ function bruteForceProtection(maxAttempts = 5, windowMs = 15 * 60 * 1000) { return async (req, res, next) => { try { // Bypass for privileged roles if (req.user && (req.user.role === 'admin' || req.user.role === 'verifier' || req.user.role === 'host' || req.user.roles?.includes('verifier') || req.user.roles?.includes('platformAdmin'))) { return next(); } const identifier = req.ip || req.connection.remoteAddress; const endpoint = req.originalUrl; const key = `bruteforce:${identifier}:${endpoint}`; const attempts = await redisClient.get(key); const attemptCount = attempts ? parseInt(attempts) : 0; if (attemptCount >= maxAttempts) { logger.warn(`Brute force attempt blocked for ${identifier} on ${endpoint}`); return res.status(429).json({ error: 'Too many failed attempts. Please try again later.', retryAfter: Math.ceil(windowMs / 1000) }); } // Increment attempt counter if (redisClient && redisClient.isOpen) { await redisClient.setEx(key, Math.ceil(windowMs / 1000), (attemptCount + 1).toString()); } next(); } catch (error) { logger.error('Brute force protection error:', error); next(); // Continue on error } }; } /** * Request size validation middleware */ function requestSizeValidation(maxSize = '1mb') { return (req, res, next) => { const contentLength = parseInt(req.headers['content-length'] || '0'); const maxSizeBytes = parseSize(maxSize); if (contentLength > maxSizeBytes) { logger.warn(`Request size exceeded: ${contentLength} bytes from ${req.ip}`); return res.status(413).json({ error: 'Request entity too large', maxSize }); } next(); }; } /** * Parse size string to bytes */ function parseSize(size) { const units = { 'b': 1, 'kb': 1024, 'mb': 1024 * 1024, 'gb': 1024 * 1024 * 1024 }; const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/); if (match) { const value = parseFloat(match[1]); const unit = match[2]; return value * units[unit]; } return parseInt(size) || 1024 * 1024; // Default to 1MB } /** * SQL injection prevention middleware */ function sqlInjectionProtection(req, res, next) { const dangerousPatterns = [ /(\b(union|select|insert|update|delete|drop|create|alter|exec|execute)\b)/i, /(--|\/\*|\*\/|xp_|sp_|@@|char\(|nchar\(|varchar\(|nvarchar\(|cast\(|convert\(|declare\s+@)/i, /(\b(and|or)\s+\d+\s*=\s*\d+)/i, /(\b(and|or)\s+['"]\w+['"]\s*=\s*['"]\w+['"])/i ]; const checkValue = (value) => { if (typeof value === 'string') { for (const pattern of dangerousPatterns) { if (pattern.test(value)) { logger.warn(`Potential SQL injection attempt from ${req.ip}: ${value}`); return false; } } } return true; }; const checkObject = (obj) => { for (const [, value] of Object.entries(obj)) { if (typeof value === 'object' && value !== null) { if (!checkObject(value)) return false; } else if (!checkValue(value)) { return false; } } return true; }; // Check body, query, and params if (req.body && !checkObject(req.body)) { return res.status(400).json({ error: 'Invalid input detected' }); } if (req.query && !checkObject(req.query)) { return res.status(400).json({ error: 'Invalid query parameters' }); } if (req.params && !checkObject(req.params)) { return res.status(400).json({ error: 'Invalid path parameters' }); } next(); } /** * NoSQL injection prevention middleware */ function noSqlInjectionProtection(req, res, next) { const dangerousPatterns = [ /\$where/i, /\$ne/i, /\$gt/i, /\$lt/i, /\$gte/i, /\$lte/i, /\$in/i, /\$nin/i, /\$regex/i, /\$options/i, /\$text/i, /\$search/i ]; const checkValue = (value) => { if (typeof value === 'string') { for (const pattern of dangerousPatterns) { if (pattern.test(value)) { logger.warn(`Potential NoSQL injection attempt from ${req.ip}: ${value}`); return false; } } } return true; }; const checkObject = (obj) => { for (const [, value] of Object.entries(obj)) { if (typeof value === 'object' && value !== null) { if (!checkObject(value)) return false; } else if (!checkValue(value)) { return false; } } return true; }; // Check body, query, and params if (req.body && !checkObject(req.body)) { return res.status(400).json({ error: 'Invalid input detected' }); } if (req.query && !checkObject(req.query)) { return res.status(400).json({ error: 'Invalid query parameters' }); } if (req.params && !checkObject(req.params)) { return res.status(400).json({ error: 'Invalid path parameters' }); } next(); } /** * XSS protection middleware */ function xssProtection(req, res, next) { const xssPatterns = [ /)<[^<]*)*<\/script>/gi, /javascript:/gi, /on\w+\s*=/gi, /)<[^<]*)*<\/iframe>/gi, /)<[^<]*)*<\/object>/gi, /)<[^<]*)*<\/embed>/gi ]; const checkValue = (value) => { if (typeof value === 'string') { for (const pattern of xssPatterns) { if (pattern.test(value)) { logger.warn(`Potential XSS attempt from ${req.ip}: ${value}`); return false; } } } return true; }; const checkObject = (obj) => { for (const [, value] of Object.entries(obj)) { if (typeof value === 'object' && value !== null) { if (!checkObject(value)) return false; } else if (!checkValue(value)) { return false; } } return true; }; // Check body, query, and params if (req.body && !checkObject(req.body)) { return res.status(400).json({ error: 'Invalid input detected' }); } if (req.query && !checkObject(req.query)) { return res.status(400).json({ error: 'Invalid query parameters' }); } if (req.params && !checkObject(req.params)) { return res.status(400).json({ error: 'Invalid path parameters' }); } next(); } /** * Security logging middleware */ function securityLogging(req, res, next) { const securityEvents = []; // Check for suspicious patterns const suspiciousPatterns = [ { pattern: /\.\.\//, name: 'Path traversal attempt' }, { pattern: /