feat: enhance security configuration and SSE management with robust token validation and client tracking
- Refactored `.env.example` with comprehensive security and configuration parameters - Added new `security.config.ts` for centralized security configuration management - Improved middleware with enhanced authentication, request validation, and error handling - Updated SSE routes and manager with advanced client tracking, rate limiting, and connection management - Implemented more granular token validation with IP-based rate limiting and connection tracking - Added detailed error responses and improved logging for security-related events
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
||||
// Rate limiter middleware
|
||||
export const rateLimiter = rateLimit({
|
||||
@@ -24,34 +25,79 @@ export const wsRateLimiter = rateLimit({
|
||||
}
|
||||
});
|
||||
|
||||
// Security headers middleware
|
||||
export const securityHeaders = (_req: Request, res: Response, next: NextFunction) => {
|
||||
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');
|
||||
// Authentication middleware
|
||||
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '') || '';
|
||||
const clientIp = req.ip || req.socket.remoteAddress || '';
|
||||
|
||||
const validationResult = TokenManager.validateToken(token, clientIp);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
error: validationResult.error || 'Invalid token',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Request validation middleware
|
||||
// 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 request validation middleware
|
||||
export const validateRequest = (req: Request, res: Response, next: NextFunction) => {
|
||||
// Skip validation for health check endpoints
|
||||
if (req.path === '/health' || req.path === '/mcp') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 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: 'Content-Type must be application/json'
|
||||
message: 'Unsupported Media Type',
|
||||
error: 'Content-Type must be application/json',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Validate request body size
|
||||
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
||||
if (contentLength > 1024 * 1024) { // 1MB limit
|
||||
const maxSize = 1024 * 1024; // 1MB limit
|
||||
if (contentLength > maxSize) {
|
||||
return res.status(413).json({
|
||||
success: false,
|
||||
message: 'Request body too large'
|
||||
message: 'Payload Too Large',
|
||||
error: `Request body must not exceed ${maxSize} bytes`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Validate request body structure
|
||||
if (req.method !== 'GET' && req.body) {
|
||||
if (typeof req.body !== 'object' || Array.isArray(req.body)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Bad Request',
|
||||
error: 'Invalid request body structure',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -84,21 +130,7 @@ export const sanitizeInput = (req: Request, _res: Response, next: NextFunction)
|
||||
next();
|
||||
};
|
||||
|
||||
// Authentication middleware
|
||||
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (!token || token !== HASS_CONFIG.TOKEN) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized - Invalid token'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Error handling middleware
|
||||
// Enhanced error handling middleware
|
||||
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error('Error:', err);
|
||||
|
||||
@@ -106,8 +138,9 @@ export const errorHandler = (err: Error, _req: Request, res: Response, _next: Ne
|
||||
if (err.name === 'ValidationError') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation error',
|
||||
details: err.message
|
||||
message: 'Validation Error',
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,15 +148,26 @@ export const errorHandler = (err: Error, _req: Request, res: Response, _next: Ne
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
details: err.message
|
||||
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',
|
||||
details: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||||
message: 'Internal Server Error',
|
||||
error: process.env.NODE_ENV === 'development' ? err.message : 'An unexpected error occurred',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user