import {inject, injectable} from '@loopback/core'; import {HttpErrors, Request, Response} from '@loopback/rest'; import {FlclLoggerLb4} from 'fc-logger'; import {DEFAULT_SECURITY_CONFIG} from '../constants/security-defaults'; import {FcSecurityComponentBindings} from '../keys'; import {SecurityConfig, SecurityValidationResult} from '../types'; // Import handlers import {CorsHandler} from '../handlers/cors.handler'; import {FileUploadHandler} from '../handlers/file-upload.handler'; import {SecurityHeadersHandler} from '../handlers/security-headers.handler'; // Import validators and detectors import {SqlInjectionDetector} from '../detectors/sql-injection.detector'; import {XssDetector} from '../detectors/xss.detector'; import {InputValidator} from '../validators/input.validator'; // Import utilities import {SecurityError} from '../utils/security-error'; import {SecurityLogger} from '../utils/security-logger'; @injectable() export class SecurityMiddleware { private corsHandler: CorsHandler; private securityHeadersHandler: SecurityHeadersHandler; private fileUploadHandler: FileUploadHandler; private inputValidator: InputValidator; private sqlInjectionDetector: SqlInjectionDetector; private xssDetector: XssDetector; private securityLogger: SecurityLogger; private config: SecurityConfig; // Ensure this is always defined after constructor constructor( @inject(FcSecurityComponentBindings.SECURITY_MIDDLEWARE_CONFIG, {optional: true}) config?: SecurityConfig, @inject(FcSecurityComponentBindings.FLCL_LOGGER, {optional: true}) private logger?: FlclLoggerLb4 ) { // Use default configuration if none provided this.config = this.mergeWithDefaults(config); this.initializeHandlers(); } /** * Create a SecurityMiddleware instance with default configuration */ static createWithDefaults( logger?: FlclLoggerLb4 ): SecurityMiddleware { return new SecurityMiddleware(undefined, logger); } /** * Create a SecurityMiddleware instance with custom configuration */ static createWithConfig( config: SecurityConfig, logger?: FlclLoggerLb4 ): SecurityMiddleware { return new SecurityMiddleware(config, logger); } private mergeWithDefaults(userConfig?: SecurityConfig): SecurityConfig { if (!userConfig) { return DEFAULT_SECURITY_CONFIG; } // Deep merge user configuration with defaults return { skip: userConfig.skip ?? DEFAULT_SECURITY_CONFIG.skip, cors: { ...DEFAULT_SECURITY_CONFIG.cors, ...userConfig.cors }, validation: { ...DEFAULT_SECURITY_CONFIG.validation, ...userConfig.validation }, headers: { ...DEFAULT_SECURITY_CONFIG.headers, ...userConfig.headers }, sqlInjection: { ...DEFAULT_SECURITY_CONFIG.sqlInjection, ...userConfig.sqlInjection }, xss: { ...DEFAULT_SECURITY_CONFIG.xss, ...userConfig.xss }, logging: { ...DEFAULT_SECURITY_CONFIG.logging, ...userConfig.logging }, fileUpload: { ...DEFAULT_SECURITY_CONFIG.fileUpload, ...userConfig.fileUpload }, errorResponse: { ...DEFAULT_SECURITY_CONFIG.errorResponse, ...userConfig.errorResponse }, enableAllProtections: userConfig.enableAllProtections ?? DEFAULT_SECURITY_CONFIG.enableAllProtections, skipOnError: userConfig.skipOnError ?? DEFAULT_SECURITY_CONFIG.skipOnError, healthCheckPaths: userConfig.healthCheckPaths ?? DEFAULT_SECURITY_CONFIG.healthCheckPaths, skipFields: userConfig.skipFields ?? DEFAULT_SECURITY_CONFIG.skipFields, skipUrls: userConfig.skipUrls ?? DEFAULT_SECURITY_CONFIG.skipUrls }; } private initializeHandlers(): void { // Initialize security logger first this.securityLogger = new SecurityLogger(this.config.logging, this.logger, this.config.errorResponse); // Initialize handlers with their respective configurations this.corsHandler = new CorsHandler( this.config.cors, this.securityLogger ); this.securityHeadersHandler = new SecurityHeadersHandler( this.config.headers, this.securityLogger ); this.fileUploadHandler = new FileUploadHandler( this.config.fileUpload, this.securityLogger ); // Initialize validators and detectors with skip fields configuration this.inputValidator = new InputValidator( this.config.validation, this.securityLogger, this.config.skipFields ); this.sqlInjectionDetector = new SqlInjectionDetector( this.config.sqlInjection, this.securityLogger, this.config.skipFields ); this.xssDetector = new XssDetector( this.config.xss, this.securityLogger, this.config.skipFields ); } /** * Check if the request is a health check request */ private isHealthCheckRequest(req: Request): boolean { const path = req.path || req.url || ''; const healthCheckPaths = this.config.healthCheckPaths ?? []; // Check if the path matches any of the configured health check paths return healthCheckPaths.some(healthPath => { // Exact match if (path === healthPath) return true; // Path starts with health check path (for sub-paths like /health/detailed) if (path.startsWith(healthPath + '/')) return true; // For paths ending with /, also check without the trailing slash if (healthPath.endsWith('/') && path === healthPath.slice(0, -1)) return true; return false; }); } /** * Check if the request URL should be skipped from security checks */ private isSkippedUrl(req: Request): boolean { const path = req.path || req.url || ''; const skipUrls = this.config.skipUrls ?? []; // Check if the path matches any of the configured skip URL patterns return skipUrls.some(skipUrl => { // Exact match if (path === skipUrl) return true; // Path starts with skip URL (for sub-paths) if (path.startsWith(skipUrl + '/')) return true; // For paths ending with /, also check without the trailing slash if (skipUrl.endsWith('/') && path === skipUrl.slice(0, -1)) return true; // Support wildcard patterns (simple implementation) if (skipUrl.includes('*')) { const regex = new RegExp('^' + skipUrl.replace(/\*/g, '.*') + '$'); return regex.test(path); } return false; }); } /** * Apply comprehensive security checks to the incoming request */ async applySecurityChecks( req: Request, res: Response ): Promise { // Skip all security checks if configured if (this.config.skip) { return { isValid: true, errors: [], warnings: ['Security checks skipped due to configuration'], metadata: { requestId: this.generateRequestId(), checks: ['SECURITY_SKIPPED'], reason: 'Security middleware configured with skip=true' } }; } // Auto-skip security checks for health check endpoints if (this.isHealthCheckRequest(req)) { const requestId = this.generateRequestId(); const path = req.path || req.url || ''; this.securityLogger?.logSecurityEvent(req, { eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', timestamp: new Date().toISOString(), requestId, metadata: { step: 'HEALTH_CHECK_AUTO_SKIP', reason: 'Health check endpoint detected', path, configuredHealthCheckPaths: this.config.healthCheckPaths } }); return { isValid: true, errors: [], warnings: ['Security checks auto-skipped for health check endpoint'], metadata: { requestId, checks: ['HEALTH_CHECK_AUTO_SKIP'], reason: 'Health check endpoint detected', path, configuredPaths: this.config.healthCheckPaths } }; } // Auto-skip security checks for configured skip URLs if (this.isSkippedUrl(req)) { const requestId = this.generateRequestId(); const path = req.path || req.url || ''; this.securityLogger?.logSecurityEvent(req, { eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', timestamp: new Date().toISOString(), requestId, metadata: { step: 'URL_AUTO_SKIP', reason: 'Skipped URL pattern detected', path, configuredSkipUrls: this.config.skipUrls } }); return { isValid: true, errors: [], warnings: ['Security checks auto-skipped for configured URL pattern'], metadata: { requestId, checks: ['URL_AUTO_SKIP'], reason: 'Skipped URL pattern detected', path, configuredPatterns: this.config.skipUrls } }; } const requestId = this.generateRequestId(); (req as unknown as Record).securityRequestId = requestId; try { // Log the start of security processing this.securityLogger.logSecurityEvent(req, { eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', timestamp: new Date().toISOString(), requestId, metadata: { step: 'SECURITY_CHECKS_STARTED' } }, 'SecurityMiddleware', 'applySecurityChecks'); // Step 1: Apply security headers (should be first) if (this.config.headers?.enabled !== false && this.config.enableAllProtections !== false) { await this.securityHeadersHandler.applyHeaders(res); } // Step 2: CORS handling if (this.config.cors?.enabled !== false && this.config.enableAllProtections !== false) { await this.corsHandler.handleCors(req, res); } // Step 3: File upload validation (if applicable) if (this.config.fileUpload?.enabled === true && (req as unknown as Record).files) { await this.fileUploadHandler.validateFileUploads(req); } // Step 4: SQL injection detection (before sanitization to avoid false positives) if (this.config.sqlInjection?.enabled !== false && this.config.sqlInjection?.enableProtection !== false && this.config.enableAllProtections !== false) { await this.sqlInjectionDetector.detectSqlInjection(req); } // Step 5: XSS detection (before sanitization to avoid false positives) if (this.config.xss?.enabled !== false && this.config.xss?.enableProtection !== false && this.config.enableAllProtections !== false) { await this.xssDetector.detectXssAttacks(req); } // Step 6: Input validation and sanitization (includes CSV injection detection) if (this.config.validation?.enabled !== false && this.config.enableAllProtections !== false) { await this.inputValidator.validateAndSanitizeInput(req); } // All checks passed this.securityLogger.logSecurityEvent(req, { eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', timestamp: new Date().toISOString(), requestId, metadata: { step: 'ALL_SECURITY_CHECKS_PASSED' } }); return { isValid: true, errors: [], metadata: { requestId, securityChecksApplied: this.getAppliedChecks() } }; } catch (error) { // Handle security violations return this.handleSecurityViolation(error, req, requestId); } } /** * Handle security violations and generate appropriate responses */ private handleSecurityViolation( error: unknown, req: Request, requestId: string ): SecurityValidationResult { const securityError = SecurityError.fromError(error); // Log security violation this.securityLogger.logSecurityEvent(req, { eventType: 'SECURITY_VIOLATION', severity: securityError.severity, timestamp: new Date().toISOString(), requestId, error: securityError.message, metadata: { errorType: securityError.type, statusCode: securityError.statusCode, originalError: error instanceof Error ? error.message : String(error) } }); // Use custom error message if provided, otherwise use default const clientMessage = this.config.errorResponse?.customErrorMessage ?? 'Request blocked by server'; const responseBody = HttpErrors[securityError.statusCode](clientMessage); responseBody.name = 'Security Violation'; responseBody.requestId = requestId; // Add detailed error information only if explicitly enabled if (this.config.errorResponse?.exposeDetailedErrors === true) { (responseBody as Record).details = { errorType: securityError.type, severity: securityError.severity, originalError: error instanceof Error ? error.message : String(error) }; } if (!this.config.skipOnError) { throw responseBody; } else return { isValid: false, errors: [securityError.message], blocked: true, reason: securityError.type, metadata: { requestId, statusCode: securityError.statusCode, severity: securityError.severity } }; } /** * Get list of security checks that are currently enabled */ private getAppliedChecks(): string[] { const checks: string[] = []; if (this.config.headers?.enabled !== false) checks.push('SECURITY_HEADERS'); if (this.config.cors?.enabled !== false) checks.push('CORS'); if (this.config.fileUpload?.enabled === true) checks.push('FILE_UPLOAD_VALIDATION'); if (this.config.validation?.enabled !== false) checks.push('INPUT_VALIDATION'); if (this.config.sqlInjection?.enabled !== false && this.config.sqlInjection?.enableProtection !== false) checks.push('SQL_INJECTION_DETECTION'); if (this.config.xss?.enabled !== false && this.config.xss?.enableProtection !== false) checks.push('XSS_DETECTION'); return checks; } /** * Generate a unique request ID for tracking */ private generateRequestId(): string { return `sec_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; } /** * Update security configuration at runtime */ updateConfiguration(newConfig: Partial): void { this.config = {...this.config, ...newConfig}; this.initializeHandlers(); } /** * Add health check paths to bypass security */ addHealthCheckPaths(paths: string[]): void { const currentPaths = this.config.healthCheckPaths ?? []; const newPaths = [...currentPaths]; for (const path of paths) { if (!newPaths.includes(path)) { newPaths.push(path); } } this.config.healthCheckPaths = newPaths; } /** * Remove health check paths */ removeHealthCheckPaths(paths: string[]): void { const currentPaths = this.config.healthCheckPaths ?? []; this.config.healthCheckPaths = currentPaths.filter(path => !paths.includes(path)); } /** * Get current health check paths */ getHealthCheckPaths(): string[] { return [...(this.config.healthCheckPaths ?? [])]; } /** * Get current security configuration */ getConfiguration(): SecurityConfig { return {...this.config}; } }