/** * File Validation Service * * Validates uploaded files for security */ import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { fileUploadConfig } from '../../infrastructure/security/security.config'; import * as path from 'path'; export interface FileValidationResult { valid: boolean; errors: string[]; } @Injectable() export class FileValidationService { private readonly logger = new Logger(FileValidationService.name); /** * Validate uploaded file */ async validateFile(file: Express.Multer.File): Promise { const errors: string[] = []; // Check if file exists if (!file) { errors.push('No file provided'); return { valid: false, errors }; } // Validate file size if (file.size > fileUploadConfig.maxFileSize) { errors.push( `File size exceeds maximum allowed size of ${fileUploadConfig.maxFileSize / 1024 / 1024}MB` ); } // Validate MIME type if (!fileUploadConfig.allowedMimeTypes.includes(file.mimetype)) { errors.push( `File type ${ file.mimetype } is not allowed. Allowed types: ${fileUploadConfig.allowedMimeTypes.join(', ')}` ); } // Validate file extension const ext = path.extname(file.originalname).toLowerCase(); if (!fileUploadConfig.allowedExtensions.includes(ext)) { errors.push( `File extension ${ext} is not allowed. Allowed extensions: ${fileUploadConfig.allowedExtensions.join( ', ' )}` ); } // Validate filename (prevent directory traversal) if (this.containsDirectoryTraversal(file.originalname)) { errors.push('Invalid filename: directory traversal detected'); } // Check for executable files disguised with double extensions if (this.hasDoubleExtension(file.originalname)) { errors.push('Invalid filename: double extension detected'); } // Validate file content matches extension (basic check) if (!this.contentMatchesExtension(file)) { errors.push('File content does not match extension'); } const valid = errors.length === 0; if (!valid) { this.logger.warn(`File validation failed: ${errors.join(', ')}`); } return { valid, errors }; } /** * Validate and sanitize filename */ sanitizeFilename(filename: string): string { // Remove path traversal attempts let sanitized = path.basename(filename); // Remove special characters except dot, dash, underscore sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '_'); // Limit filename length const ext = path.extname(sanitized); const name = path.basename(sanitized, ext); if (name.length > 100) { sanitized = name.substring(0, 100) + ext; } return sanitized; } /** * Check for directory traversal attempts */ private containsDirectoryTraversal(filename: string): boolean { return ( filename.includes('../') || filename.includes('..\\') || filename.includes('..\\') || filename.includes('%2e%2e') || filename.includes('0x2e0x2e') ); } /** * Check for double extensions (e.g., file.pdf.exe) */ private hasDoubleExtension(filename: string): boolean { const dangerousExtensions = [ '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', '.msi', '.app', '.deb', '.rpm', ]; const lowerFilename = filename.toLowerCase(); return dangerousExtensions.some(ext => lowerFilename.includes(ext)); } /** * Basic check if file content matches its extension */ private contentMatchesExtension(file: Express.Multer.File): boolean { const ext = path.extname(file.originalname).toLowerCase(); const buffer = file.buffer; if (!buffer || buffer.length < 4) { return false; } // Check file signatures (magic numbers) const signatures: Record = { '.pdf': [0x25, 0x50, 0x44, 0x46], // %PDF '.jpg': [0xff, 0xd8, 0xff], '.jpeg': [0xff, 0xd8, 0xff], '.png': [0x89, 0x50, 0x4e, 0x47], '.webp': [0x52, 0x49, 0x46, 0x46], // RIFF (need to check WEBP later) '.xlsx': [0x50, 0x4b, 0x03, 0x04], // ZIP format '.xls': [0xd0, 0xcf, 0x11, 0xe0], // OLE2 format }; const expectedSignature = signatures[ext]; if (!expectedSignature) { // For unknown extensions, assume valid (CSV, etc.) return true; } // Check if buffer starts with expected signature for (let i = 0; i < expectedSignature.length; i++) { if (buffer[i] !== expectedSignature[i]) { return false; } } return true; } /** * Scan file for viruses (placeholder for production virus scanning) */ async scanForViruses(file: Express.Multer.File): Promise { if (!fileUploadConfig.scanForViruses) { return true; // Skip in development } // TODO: Integrate with ClamAV or similar virus scanner // For now, just log this.logger.log(`Virus scan requested for file: ${file.originalname} (not implemented)`); return true; } /** * Validate multiple files */ async validateFiles(files: Express.Multer.File[]): Promise { const allErrors: string[] = []; for (const file of files) { const result = await this.validateFile(file); if (!result.valid) { allErrors.push(...result.errors); } } return { valid: allErrors.length === 0, errors: allErrors, }; } }