refactor: enhance middleware and security with advanced protection mechanisms

- Upgraded rate limiter configuration with more granular control and detailed headers
- Improved authentication middleware with enhanced token validation and error responses
- Implemented advanced input sanitization using sanitize-html with comprehensive XSS protection
- Replaced manual security headers with helmet for robust web security configuration
- Enhanced error handling middleware with more detailed logging and specific error type handling
- Updated SSE rate limiting with burst and window-based restrictions
- Improved token validation with more precise signature and claim verification
This commit is contained in:
jango-blockchained
2025-02-03 22:29:41 +01:00
parent 89f2278c25
commit 10bf5919e4
3 changed files with 352 additions and 269 deletions

View File

@@ -2,32 +2,52 @@ import { Request, Response, NextFunction } from 'express';
import { HASS_CONFIG, RATE_LIMIT_CONFIG } from '../config/index.js';
import rateLimit from 'express-rate-limit';
import { TokenManager } from '../security/index.js';
import sanitizeHtml from 'sanitize-html';
import helmet from 'helmet';
// Rate limiter middleware
// Rate limiter middleware with enhanced configuration
export const rateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: RATE_LIMIT_CONFIG.REGULAR,
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: {
success: false,
message: 'Too many requests, please try again later.',
reset_time: new Date(Date.now() + 60 * 1000).toISOString()
}
},
skipSuccessfulRequests: false, // Count all requests
keyGenerator: (req) => req.ip || req.socket.remoteAddress || 'unknown' // Use IP for rate limiting
});
// WebSocket rate limiter middleware
// WebSocket rate limiter middleware with enhanced configuration
export const wsRateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: RATE_LIMIT_CONFIG.WEBSOCKET,
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
message: 'Too many WebSocket connections, please try again later.',
reset_time: new Date(Date.now() + 60 * 1000).toISOString()
}
},
skipSuccessfulRequests: false,
keyGenerator: (req) => req.ip || req.socket.remoteAddress || 'unknown'
});
// Authentication middleware
// Authentication middleware with enhanced security
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '') || '';
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Unauthorized',
error: 'Missing or invalid authorization header',
timestamp: new Date().toISOString()
});
}
const token = authHeader.replace('Bearer ', '');
const clientIp = req.ip || req.socket.remoteAddress || '';
const validationResult = TokenManager.validateToken(token, clientIp);
@@ -44,18 +64,40 @@ export const authenticate = (req: Request, res: Response, next: NextFunction) =>
next();
};
// Enhanced security headers middleware
export const securityHeaders = (_req: Request, res: Response, next: NextFunction) => {
// Set strict 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('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss: https:;");
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
};
// Enhanced security headers middleware using helmet
export const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'wss:', 'https:'],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"]
}
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'same-origin' },
dnsPrefetchControl: { allow: false },
frameguard: { action: 'deny' },
hidePoweredBy: true,
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
ieNoOpen: true,
noSniff: true,
originAgentCluster: true,
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
xssFilter: true
});
// Enhanced request validation middleware
export const validateRequest = (req: Request, res: Response, next: NextFunction) => {
@@ -65,13 +107,16 @@ export const validateRequest = (req: Request, res: Response, next: NextFunction)
}
// Validate content type for POST/PUT/PATCH requests
if (['POST', 'PUT', 'PATCH'].includes(req.method) && !req.is('application/json')) {
return res.status(415).json({
success: false,
message: 'Unsupported Media Type',
error: 'Content-Type must be application/json',
timestamp: new Date().toISOString()
});
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
const contentType = req.headers['content-type'];
if (!contentType || !contentType.includes('application/json')) {
return res.status(415).json({
success: false,
message: 'Unsupported Media Type',
error: 'Content-Type must be application/json',
timestamp: new Date().toISOString()
});
}
}
// Validate request body size
@@ -101,23 +146,42 @@ export const validateRequest = (req: Request, res: Response, next: NextFunction)
next();
};
// Input sanitization middleware
// Enhanced input sanitization middleware
export const sanitizeInput = (req: Request, _res: Response, next: NextFunction) => {
if (req.body) {
// Recursively sanitize object
const sanitizeObject = (obj: any): any => {
const sanitizeValue = (value: unknown): unknown => {
if (typeof value === 'string') {
// Sanitize HTML content
return sanitizeHtml(value, {
allowedTags: [], // Remove all HTML tags
allowedAttributes: {}, // Remove all attributes
textFilter: (text) => {
// Remove potential XSS patterns
return text.replace(/javascript:/gi, '')
.replace(/data:/gi, '')
.replace(/vbscript:/gi, '')
.replace(/on\w+=/gi, '')
.replace(/\b(alert|confirm|prompt|exec|eval|setTimeout|setInterval)\b/gi, '');
}
});
}
return value;
};
const sanitizeObject = (obj: unknown): unknown => {
if (typeof obj !== 'object' || obj === null) {
return obj;
return sanitizeValue(obj);
}
if (Array.isArray(obj)) {
return obj.map(item => sanitizeObject(item));
}
const sanitized: any = {};
for (const [key, value] of Object.entries(obj)) {
// Remove any potentially dangerous characters from keys
const sanitizedKey = key.replace(/[<>]/g, '');
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
// Sanitize keys
const sanitizedKey = typeof key === 'string' ? sanitizeValue(key) as string : key;
// Recursively sanitize values
sanitized[sanitizedKey] = sanitizeObject(value);
}
@@ -131,44 +195,68 @@ export const sanitizeInput = (req: Request, _res: Response, next: NextFunction)
};
// Enhanced error handling middleware
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error('Error:', err);
// Handle specific error types
if (err.name === 'ValidationError') {
return res.status(400).json({
success: false,
message: 'Validation Error',
error: err.message,
timestamp: new Date().toISOString()
});
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({
success: false,
message: 'Unauthorized',
error: err.message,
timestamp: new Date().toISOString()
});
}
if (err.name === 'ForbiddenError') {
return res.status(403).json({
success: false,
message: 'Forbidden',
error: err.message,
timestamp: new Date().toISOString()
});
}
// Default error response
res.status(500).json({
success: false,
message: 'Internal Server Error',
error: process.env.NODE_ENV === 'development' ? err.message : 'An unexpected error occurred',
export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction) => {
// Log error with request context
console.error('Error:', {
error: err.message,
stack: err.stack,
method: req.method,
path: req.path,
ip: req.ip,
timestamp: new Date().toISOString()
});
// Handle specific error types
switch (err.name) {
case 'ValidationError':
return res.status(400).json({
success: false,
message: 'Validation Error',
error: err.message,
timestamp: new Date().toISOString()
});
case 'UnauthorizedError':
return res.status(401).json({
success: false,
message: 'Unauthorized',
error: err.message,
timestamp: new Date().toISOString()
});
case 'ForbiddenError':
return res.status(403).json({
success: false,
message: 'Forbidden',
error: err.message,
timestamp: new Date().toISOString()
});
case 'NotFoundError':
return res.status(404).json({
success: false,
message: 'Not Found',
error: err.message,
timestamp: new Date().toISOString()
});
case 'ConflictError':
return res.status(409).json({
success: false,
message: 'Conflict',
error: err.message,
timestamp: new Date().toISOString()
});
default:
// Default error response
return res.status(500).json({
success: false,
message: 'Internal Server Error',
error: process.env.NODE_ENV === 'development' ? err.message : 'An unexpected error occurred',
timestamp: new Date().toISOString()
});
}
};
// Export all middleware