import {inject, injectable, Provider, ValueOrPromise} from '@loopback/core'; import {Request} from '@loopback/rest'; import {FlclError, FlclLoggerLb4} from 'fc-logger'; import {FcSecurityComponentBindings} from '../keys'; import {SecurityValidationResult} from '../types'; export type LoggerConfig = { className: string; methodName: string; }; /** * Security validator provider that provides comprehensive security validation */ @injectable() export class ValidatorService implements Provider { constructor( @inject(FcSecurityComponentBindings.FLCL_LOGGER, {optional: true}) private logger?: FlclLoggerLb4, @inject(FcSecurityComponentBindings.FLCL_ERROR, {optional: true}) private flclError?: FlclError ) { } value(): ValueOrPromise { return this; } /** * Validate entire request for security threats */ async validateSecurityRequest(request: Request): Promise { const errors: string[] = []; const warnings: string[] = []; try { // Validate request body const bodyResult = await this.validateRequestBody(request); if (!bodyResult.isValid) { errors.push(...bodyResult.errors); warnings.push(...(bodyResult.warnings ?? [])); } // Validate query parameters const queryResult = await this.validateQueryParams(request); if (!queryResult.isValid) { errors.push(...queryResult.errors); warnings.push(...(queryResult.warnings ?? [])); } // Validate URL parameters const paramsResult = await this.validateUrlParams(request); if (!paramsResult.isValid) { errors.push(...paramsResult.errors); warnings.push(...(paramsResult.warnings ?? [])); } const isValid = errors.length === 0; if (!isValid && this.logger) { this.logger.error({ className: 'ValidatorService', methodName: 'validateSecurityRequest', message: 'Security validation failed', data: { errors, warnings, url: request.url, method: request.method, rootRequestId: request.headers?.['root-request-id'] ?? request.headers?.['x-root-request-id'] } }); } return { isValid, errors, warnings, blocked: !isValid, reason: !isValid ? 'Security validation failed' : undefined, metadata: { component: 'ValidatorService', validatedAt: new Date().toISOString() } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (this.logger) { this.logger.error({ className: 'ValidatorService', methodName: 'validateSecurityRequest', message: errorMessage, data: { url: request.url, method: request.method, error: errorMessage, rootRequestId: request.headers?.['root-request-id'] ?? request.headers?.['x-root-request-id'] } }); } return { isValid: false, errors: [errorMessage], warnings: [], blocked: true, reason: 'Validation error', metadata: { component: 'SecurityValidationProvider', error: errorMessage } }; } } /** * Validate request body for security threats */ async validateRequestBody(request: Request): Promise { if (!request.body) { return { isValid: true, errors: [], warnings: [] }; } return this.validateObjectSecurity(request.body as Record, 'request.body'); } /** * Validate query parameters for security threats */ async validateQueryParams(request: Request): Promise { if (!request.query || Object.keys(request.query).length === 0) { return { isValid: true, errors: [], warnings: [] }; } return this.validateObjectSecurity(request.query as Record, 'request.query'); } /** * Validate URL parameters for security threats */ async validateUrlParams(request: Request): Promise { const params = (request as unknown as {params?: Record}).params; if (!params || Object.keys(params).length === 0) { return { isValid: true, errors: [], warnings: [] }; } return this.validateObjectSecurity(params, 'request.params'); } /** * Legacy validate method for backward compatibility */ async validate(request: Request, loggerConfig?: LoggerConfig): Promise { const result = await this.validateSecurityRequest(request); if (!result.isValid) { // Log validation failures if (this.logger) { this.logger.error({ className: loggerConfig?.className ?? 'SecurityValidationProvider', methodName: loggerConfig?.methodName ?? 'validate', message: 'Security validation failed', data: { errors: result.errors, warnings: result.warnings, url: request.url, method: request.method } }); } // Throw error for validation failure const error = this.flclError?.createError( 'Security validation failed', 'SECURITY_VALIDATION_ERROR', 400, { errors: result.errors, warnings: result.warnings } ) ?? new Error(`Security validation failed: ${result.errors.join(', ')}`); throw error; } } /** * Validate object for common security threats */ private validateObjectSecurity(obj: Record, context: string): SecurityValidationResult { const errors: string[] = []; const warnings: string[] = []; try { // Check for SQL injection patterns this.checkForSQLInjectionInObject(obj, errors, context); // Check for XSS patterns this.checkForXSSInObject(obj, errors, context); // Check for oversized data this.checkForOversizedData(obj, errors, warnings, context); return { isValid: errors.length === 0, errors, warnings, blocked: errors.length > 0, reason: errors.length > 0 ? 'Security threats detected' : undefined }; } catch (error) { const errorMsg = `Validation error in ${context}: ${error instanceof Error ? error.message : String(error)}`; return { isValid: false, errors: [errorMsg], warnings: [], blocked: true, reason: 'Validation exception' }; } } /** * Check for SQL injection patterns in object */ private checkForSQLInjectionInObject(obj: Record, errors: string[], context: string): void { const sqlPatterns = [ /(\b(union|select|insert|update|delete|drop|create|alter|exec|execute)\b)/gi, /('|(\\')|(;)|(\\)|(\-\-)|(%27)|(%3D)|(%3B)|(%22)|(\+))/gi ]; this.traverseObjectForPatterns(obj, sqlPatterns, (path, pattern) => { errors.push(`Potential SQL injection detected in ${context}.${path} (pattern: ${pattern})`); }); } /** * Check for XSS patterns in object */ private checkForXSSInObject(obj: Record, errors: string[], context: string): void { const xssPatterns = [ /)<[^<]*)*<\/script>/gi, /)<[^<]*)*<\/iframe>/gi, /javascript:/gi, /on\w+\s*=/gi ]; this.traverseObjectForPatterns(obj, xssPatterns, (path, pattern) => { errors.push(`Potential XSS detected in ${context}.${path} (pattern: ${pattern})`); }); } /** * Check for oversized data that could indicate DoS attempt */ private checkForOversizedData(obj: Record, errors: string[], warnings: string[], context: string): void { const maxStringLength = 10000; // 10KB limit for individual strings const maxObjectDepth = 10; const warningStringLength = 5000; // Warn at 5KB this.traverseObjectForSize(obj, (path, value, depth) => { if (typeof value === 'string') { if (value.length > maxStringLength) { errors.push(`Oversized string in ${context}.${path} (${value.length} chars, max: ${maxStringLength})`); } else if (value.length > warningStringLength) { warnings.push(`Large string in ${context}.${path} (${value.length} chars, recommended max: ${warningStringLength})`); } } if (depth > maxObjectDepth) { errors.push(`Object nesting too deep in ${context}.${path} (depth: ${depth}, max: ${maxObjectDepth})`); } }); } /** * Traverse object looking for regex patterns */ private traverseObjectForPatterns( obj: Record, patterns: RegExp[], callback: (path: string, pattern: string) => void, path = '' ): void { for (const [key, value] of Object.entries(obj)) { const currentPath = path ? `${path}.${key}` : key; if (typeof value === 'string') { for (const pattern of patterns) { const match = pattern.exec(value); if (match) { callback(currentPath, match[0]); pattern.lastIndex = 0; // Reset regex state } } } else if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { value.forEach((item, index) => { if (typeof item === 'object' && item !== null) { this.traverseObjectForPatterns(item as Record, patterns, callback, `${currentPath}[${index}]`); } }); } else { this.traverseObjectForPatterns(value as Record, patterns, callback, currentPath); } } } } /** * Traverse object checking for size issues */ private traverseObjectForSize( obj: Record, callback: (path: string, value: unknown, depth: number) => void, path = '', depth = 0 ): void { for (const [key, value] of Object.entries(obj)) { const currentPath = path ? `${path}.${key}` : key; callback(currentPath, value, depth); if (typeof value === 'object' && value !== null && depth < 15) { // Prevent infinite recursion if (Array.isArray(value)) { value.forEach((item, index) => { if (typeof item === 'object' && item !== null) { this.traverseObjectForSize(item as Record, callback, `${currentPath}[${index}]`, depth + 1); } }); } else { this.traverseObjectForSize(value as Record, callback, currentPath, depth + 1); } } } } } // Export the main class