import {Request} from '@loopback/rest'; import {LoggerConfig, ValidatorService} from '../providers/validator.provider'; import {CorsConfig, ErrorResponseConfig, FileUploadConfig, LoggingConfig, SecurityHeadersConfig, SqlInjectionConfig, ValidationConfig, XssConfig} from '../types'; type SecurityValidationOptions = { skip?: boolean; skipOnError?: boolean; enableAllProtections?: boolean; logger?: LoggerConfig; validateBody?: boolean; validateQuery?: boolean; validateParams?: boolean; cors?: CorsConfig; validation?: ValidationConfig; headers?: SecurityHeadersConfig; sqlInjection?: SqlInjectionConfig; xss?: XssConfig; logging?: LoggingConfig; fileUpload?: FileUploadConfig; errorResponse?: ErrorResponseConfig; } /** * LoopBack 4 Security validation decorator * Renamed from @validate to @securityValidation for better clarity * * For LoopBack 4, it's recommended to use interceptors instead * of this decorator for better integration with the framework. * * However, this decorator can still be used for method-level validation. * * Usage in LoopBack 4 controller: * ```typescript * export class UserController { * constructor( * @inject(RestBindings.Http.REQUEST) private request: Request, * ) {} * * @securityValidation() * @post('/users') * async createUser( * @requestBody() userData: CreateUserRequest, * ): Promise { * // Security validation happens automatically before this method executes * return this.userService.create(userData); * } * } * ``` * * @param options - Comprehensive security validation options * @returns Method decorator function */ export function securityValidation(options: SecurityValidationOptions = {}) { return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { // Store the original method const originalMethod = descriptor.value; // Replace the method with security-enhanced version descriptor.value = async function (this: {request?: Request}, ...args: unknown[]) { // Skip validation if configured if (options.skip) { return originalMethod.apply(this, args); } try { // Get request object from context const request = this.request; if (!request) { // If no request object, proceed without validation (for non-HTTP methods) return originalMethod.apply(this, args); } // Create security validator instance with comprehensive config const validator = new ValidatorService(); // Apply comprehensive security validation based on options if (options.enableAllProtections || (!('validateBody' in options) && !('validateQuery' in options) && !('validateParams' in options))) { // Default: enable all protections if no specific options provided await validator.validateSecurityRequest(request); } else { // Specific validation based on individual options if (options.validateBody !== false) { await validator.validateRequestBody(request); } if (options.validateQuery !== false) { await validator.validateQueryParams(request); } if (options.validateParams !== false) { await validator.validateUrlParams(request); } } // If validation passes, execute the original method return originalMethod.apply(this, args); } catch (error) { // Handle error based on skipOnError option if (options.skipOnError) { console.warn(`Security validation failed for ${propertyKey} but continuing due to skipOnError:`, error); return originalMethod.apply(this, args); } // Log the security violation if (options.logging?.enabled !== false) { console.error(`Security validation failed for ${propertyKey}:`, error); } // Re-throw the error to be handled by the framework throw error; } }; return descriptor; }; } /** * Legacy decorator name for backward compatibility * @deprecated Use @securityValidation instead */ export function validate(options: SecurityValidationOptions = {}) { console.warn('@validate decorator is deprecated. Use @securityValidation instead.'); return securityValidation(options); } /** * Comprehensive security validation decorator with all security checks enabled */ export function comprehensiveSecurity(options: Omit = {}) { return securityValidation({ ...options, enableAllProtections: true, validateBody: true, validateQuery: true, validateParams: true }); } /** * Body-only security validation decorator */ export function bodySecurityValidation(options: Omit = {}) { return securityValidation({ ...options, validateQuery: false, validateParams: false }); } /** * Query-only security validation decorator */ export function querySecurityValidation(options: Omit = {}) { return securityValidation({ ...options, validateBody: false, validateParams: false }); } /** * Parameters-only security validation decorator */ export function paramsSecurityValidation(options: Omit = {}) { return securityValidation({ ...options, validateBody: false, validateQuery: false }); } /** * SQL Injection protection only decorator */ export function sqlInjectionProtection(options: Pick = {}) { return securityValidation({ ...options, validateBody: true, validateQuery: true, validateParams: true, sqlInjection: {enableProtection: true, ...options.sqlInjection} }); } /** * XSS protection only decorator */ export function xssProtection(options: Pick = {}) { return securityValidation({ ...options, validateBody: true, validateQuery: true, validateParams: true, xss: {enableProtection: true, ...options.xss} }); } /** * File upload validation decorator */ export function fileUploadValidation(options: Pick = {}) { return securityValidation({ ...options, fileUpload: {enabled: true, ...options.fileUpload} }); } /** * Security validation interceptor class for LoopBack 4 * This is the recommended approach for LoopBack 4 applications */ export class SecurityValidationInterceptor { private validator: ValidatorService; constructor() { this.validator = new ValidatorService(); } /** * Intercept and validate requests */ async intercept(request: Request): Promise { await this.validator.validateSecurityRequest(request); } /** * Get validator instance for direct use */ getValidator(): ValidatorService { return this.validator; } } // Export types for external use export type {LoggerConfig, SecurityValidationOptions};