← back to Watches
middleware/security.js
654 lines
/**
* Security Middleware - Enterprise-Grade Security Implementation
* Implements OWASP Top 10 protections and industry best practices
*/
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import mongoSanitize from 'express-mongo-sanitize';
import { body, validationResult, param, query } from 'express-validator';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
// ============================================================================
// RATE LIMITING - Protection against brute force and DDoS attacks
// ============================================================================
/**
* General API rate limiter - 100 requests per 15 minutes per IP
* OWASP Recommendation: Implement rate limiting on all API endpoints
*/
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP, please try again after 15 minutes',
retryAfter: '15 minutes'
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: (req, res) => {
securityLogger.logRateLimitExceeded(req);
res.status(429).json({
error: 'Rate limit exceeded',
message: 'Too many requests from this IP address',
retryAfter: '15 minutes'
});
}
});
/**
* Strict rate limiter for authentication endpoints - 5 attempts per 15 minutes
* Prevents brute force attacks on login/admin endpoints
*/
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: {
error: 'Too many authentication attempts, please try again after 15 minutes',
retryAfter: '15 minutes'
},
skipSuccessfulRequests: true, // Don't count successful requests
handler: (req, res) => {
securityLogger.logBruteForceAttempt(req);
res.status(429).json({
error: 'Authentication rate limit exceeded',
message: 'Too many failed login attempts',
retryAfter: '15 minutes',
accountLocked: true
});
}
});
/**
* Lenient rate limiter for read-only operations - 200 requests per 15 minutes
*/
export const readLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 200,
message: 'Too many read requests, please slow down'
});
/**
* Strict rate limiter for write operations - 20 requests per 15 minutes
*/
export const writeLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 20,
message: 'Too many write requests, please slow down'
});
// ============================================================================
// HELMET SECURITY HEADERS - Comprehensive HTTP security headers
// ============================================================================
/**
* Configure Helmet with strict security policies
* Implements multiple layers of browser-side security
*/
export const helmetConfig = helmet({
// Content Security Policy - Prevents XSS attacks
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"'unsafe-inline'", // Required for React inline scripts
"'unsafe-eval'", // Required for development
"https://cdn.jsdelivr.net" // For Chart.js and other CDN libraries
],
styleSrc: [
"'self'",
"'unsafe-inline'", // Required for Tailwind and inline styles
"https://fonts.googleapis.com"
],
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
imgSrc: ["'self'", "data:", "https:", "blob:"],
connectSrc: [
"'self'",
"ws://45.61.58.125:7600", // WebSocket connection
"wss://45.61.58.125:7600",
"http://45.61.58.125:7600",
"https://45.61.58.125:7600"
],
frameSrc: ["'none'"], // Prevent clickjacking
objectSrc: ["'none'"], // Disable plugins
upgradeInsecureRequests: [] // Upgrade HTTP to HTTPS
}
},
// HTTP Strict Transport Security - Force HTTPS
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
// X-Frame-Options - Prevent clickjacking
frameguard: {
action: 'deny'
},
// X-Content-Type-Options - Prevent MIME type sniffing
noSniff: true,
// X-XSS-Protection - Enable browser XSS filter
xssFilter: true,
// Referrer-Policy - Control referrer information
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
},
// Permissions-Policy - Control browser features
permittedCrossDomainPolicies: {
permittedPolicies: 'none'
},
// Hide X-Powered-By header
hidePoweredBy: true
});
// ============================================================================
// INPUT VALIDATION - Prevent injection attacks
// ============================================================================
/**
* Validation rules for watch ID parameters
*/
export const validateWatchId = [
param('id')
.trim()
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage('Invalid watch ID format')
.isLength({ min: 1, max: 100 })
.withMessage('Watch ID must be between 1 and 100 characters')
];
/**
* Validation rules for search queries
*/
export const validateSearchQuery = [
query('q')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('Search query too long')
.matches(/^[a-zA-Z0-9\s\-_.]+$/)
.withMessage('Search query contains invalid characters'),
query('page')
.optional()
.isInt({ min: 1, max: 1000 })
.withMessage('Invalid page number'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Invalid limit value')
];
/**
* Validation rules for price filters
*/
export const validatePriceFilter = [
query('minPrice')
.optional()
.isInt({ min: 0, max: 10000000 })
.withMessage('Invalid minimum price'),
query('maxPrice')
.optional()
.isInt({ min: 0, max: 10000000 })
.withMessage('Invalid maximum price')
];
/**
* Validation rules for watchlist operations
*/
export const validateWatchlistOperation = [
body('userId')
.trim()
.notEmpty()
.withMessage('User ID is required')
.isLength({ max: 100 })
.withMessage('User ID too long')
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage('Invalid user ID format'),
body('watchId')
.trim()
.notEmpty()
.withMessage('Watch ID is required')
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage('Invalid watch ID format'),
body('action')
.trim()
.isIn(['add', 'remove'])
.withMessage('Action must be either "add" or "remove"')
];
/**
* Validation rules for compare operation
*/
export const validateCompareRequest = [
body('watchIds')
.isArray({ min: 1, max: 5 })
.withMessage('watchIds must be an array of 1-5 watch IDs'),
body('watchIds.*')
.trim()
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage('Invalid watch ID format in array')
];
/**
* Middleware to check validation results
*/
export const checkValidationResult = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
securityLogger.logValidationError(req, errors.array());
return res.status(400).json({
error: 'Validation failed',
details: errors.array()
});
}
next();
};
// ============================================================================
// SANITIZATION - Remove malicious content
// ============================================================================
/**
* Sanitize MongoDB queries to prevent NoSQL injection
*/
export const sanitizeInput = mongoSanitize({
replaceWith: '_',
onSanitize: ({ req, key }) => {
securityLogger.logSanitization(req, key);
}
});
/**
* XSS Protection - Clean user input
*/
export const xssProtection = (req, res, next) => {
// Clean all string inputs
const cleanString = (str) => {
if (typeof str !== 'string') return str;
return str
.replace(/[<>]/g, '') // Remove < and >
.replace(/javascript:/gi, '') // Remove javascript: protocol
.replace(/on\w+\s*=/gi, '') // Remove event handlers
.trim();
};
// Recursively clean object
const cleanObject = (obj) => {
if (!obj || typeof obj !== 'object') return obj;
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
obj[key] = cleanString(obj[key]);
} else if (typeof obj[key] === 'object') {
cleanObject(obj[key]);
}
});
return obj;
};
// Clean all input sources
req.body = cleanObject(req.body);
req.query = cleanObject(req.query);
req.params = cleanObject(req.params);
next();
};
// ============================================================================
// AUTHENTICATION & AUTHORIZATION
// ============================================================================
// JWT Secret - In production, use environment variable
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
const JWT_EXPIRY = '24h';
/**
* Generate API key for analytics endpoints
*/
export const generateApiKey = () => {
return crypto.randomBytes(32).toString('hex');
};
/**
* Verify API key for protected analytics endpoints
*/
export const verifyApiKey = (req, res, next) => {
const apiKey = req.headers['x-api-key'] || req.query.apiKey;
if (!apiKey) {
securityLogger.logUnauthorizedAccess(req, 'Missing API key');
return res.status(401).json({
error: 'Unauthorized',
message: 'API key is required for this endpoint'
});
}
// In production, verify against database
const validApiKeys = process.env.VALID_API_KEYS
? process.env.VALID_API_KEYS.split(',')
: [];
if (!validApiKeys.includes(apiKey)) {
securityLogger.logUnauthorizedAccess(req, 'Invalid API key');
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid API key'
});
}
next();
};
/**
* JWT Authentication middleware for admin endpoints
*/
export const authenticateJWT = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1]; // Bearer TOKEN
if (!token) {
securityLogger.logUnauthorizedAccess(req, 'Missing JWT token');
return res.status(401).json({
error: 'Unauthorized',
message: 'Authentication token is required'
});
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
securityLogger.logUnauthorizedAccess(req, 'Invalid JWT token');
return res.status(403).json({
error: 'Forbidden',
message: 'Invalid or expired token'
});
}
};
/**
* Generate JWT token (for admin login)
*/
export const generateJWT = (userId, role = 'admin') => {
return jwt.sign(
{ userId, role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
);
};
// ============================================================================
// SECURITY LOGGING - Track security events
// ============================================================================
class SecurityLogger {
constructor() {
this.events = [];
this.maxEvents = 10000; // Keep last 10k events in memory
}
log(event) {
const logEntry = {
timestamp: new Date().toISOString(),
...event
};
this.events.push(logEntry);
// Keep only last N events
if (this.events.length > this.maxEvents) {
this.events.shift();
}
// In production, send to external logging service
console.log(`[SECURITY] ${JSON.stringify(logEntry)}`);
}
logRateLimitExceeded(req) {
this.log({
type: 'RATE_LIMIT_EXCEEDED',
severity: 'WARNING',
ip: req.ip,
path: req.path,
method: req.method,
userAgent: req.get('user-agent')
});
}
logBruteForceAttempt(req) {
this.log({
type: 'BRUTE_FORCE_ATTEMPT',
severity: 'CRITICAL',
ip: req.ip,
path: req.path,
method: req.method,
userAgent: req.get('user-agent')
});
}
logValidationError(req, errors) {
this.log({
type: 'VALIDATION_ERROR',
severity: 'WARNING',
ip: req.ip,
path: req.path,
method: req.method,
errors: errors
});
}
logSanitization(req, key) {
this.log({
type: 'INPUT_SANITIZED',
severity: 'INFO',
ip: req.ip,
path: req.path,
sanitizedKey: key
});
}
logUnauthorizedAccess(req, reason) {
this.log({
type: 'UNAUTHORIZED_ACCESS',
severity: 'HIGH',
ip: req.ip,
path: req.path,
method: req.method,
reason: reason,
userAgent: req.get('user-agent')
});
}
logSuspiciousActivity(req, description) {
this.log({
type: 'SUSPICIOUS_ACTIVITY',
severity: 'HIGH',
ip: req.ip,
path: req.path,
method: req.method,
description: description,
userAgent: req.get('user-agent')
});
}
getRecentEvents(limit = 100) {
return this.events.slice(-limit).reverse();
}
getEventsByType(type) {
return this.events.filter(e => e.type === type);
}
getEventsBySeverity(severity) {
return this.events.filter(e => e.severity === severity);
}
getSecurityStats() {
const now = Date.now();
const oneHourAgo = now - (60 * 60 * 1000);
const recentEvents = this.events.filter(e =>
new Date(e.timestamp).getTime() > oneHourAgo
);
const stats = {
total: this.events.length,
lastHour: recentEvents.length,
byType: {},
bySeverity: {},
criticalAlerts: 0
};
recentEvents.forEach(event => {
stats.byType[event.type] = (stats.byType[event.type] || 0) + 1;
stats.bySeverity[event.severity] = (stats.bySeverity[event.severity] || 0) + 1;
if (event.severity === 'CRITICAL') {
stats.criticalAlerts++;
}
});
return stats;
}
}
export const securityLogger = new SecurityLogger();
// ============================================================================
// CORS CONFIGURATION - Control cross-origin requests
// ============================================================================
export const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, Postman, etc.)
if (!origin) return callback(null, true);
// In production, whitelist specific domains
const whitelist = [
'http://45.61.58.125:7600',
'https://45.61.58.125:7600',
'http://localhost:7600',
'http://localhost:3000'
];
if (whitelist.indexOf(origin) !== -1 || process.env.NODE_ENV === 'development') {
callback(null, true);
} else {
securityLogger.logSuspiciousActivity(
{ ip: origin },
`CORS blocked origin: ${origin}`
);
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
exposedHeaders: ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset']
};
// ============================================================================
// ADDITIONAL SECURITY MIDDLEWARE
// ============================================================================
/**
* HTTP Parameter Pollution (HPP) Protection
*/
export const hppProtection = (req, res, next) => {
// Remove duplicate parameters
['query', 'body', 'params'].forEach(source => {
if (req[source]) {
Object.keys(req[source]).forEach(key => {
if (Array.isArray(req[source][key])) {
// Keep only the last value
req[source][key] = req[source][key][req[source][key].length - 1];
}
});
}
});
next();
};
/**
* Request ID middleware - Track requests for debugging
*/
export const requestIdMiddleware = (req, res, next) => {
req.id = crypto.randomUUID();
res.setHeader('X-Request-ID', req.id);
next();
};
/**
* Security headers middleware - Additional custom headers
*/
export const additionalSecurityHeaders = (req, res, next) => {
// Prevent caching of sensitive data
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');
// Additional security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
};
/**
* WebSocket authentication
*/
export const authenticateWebSocket = (ws, req) => {
const token = new URL(req.url, 'ws://localhost').searchParams.get('token');
if (!token) {
ws.close(4001, 'Authentication required');
return false;
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
ws.userId = decoded.userId;
return true;
} catch (error) {
ws.close(4001, 'Invalid token');
return false;
}
};
export default {
apiLimiter,
authLimiter,
readLimiter,
writeLimiter,
helmetConfig,
validateWatchId,
validateSearchQuery,
validatePriceFilter,
validateWatchlistOperation,
validateCompareRequest,
checkValidationResult,
sanitizeInput,
xssProtection,
verifyApiKey,
authenticateJWT,
generateJWT,
generateApiKey,
securityLogger,
corsOptions,
hppProtection,
requestIdMiddleware,
additionalSecurityHeaders,
authenticateWebSocket
};