// Input validation and sanitization utilities export class Validator { // Sanitize HTML to prevent XSS attacks static sanitizeHTML(input: string): string { // Create a temporary div element const temp = document.createElement('div'); temp.textContent = input; return temp.innerHTML; } // Validate and sanitize text input static sanitizeText(input: string, maxLength: number = 10000): string { if (typeof input !== 'string') { return ''; } // Trim and limit length let sanitized = input.trim().substring(0, maxLength); // Remove null bytes sanitized = sanitized.replace(/\0/g, ''); // Remove control characters except newlines and tabs sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); return sanitized; } // Validate language code static validateLanguageCode(code: string, allowedLanguages: string[]): string | null { if (!code || typeof code !== 'string') { return null; } const sanitized = code.trim().toLowerCase(); // Check if it's in the allowed list if (allowedLanguages.includes(sanitized) || sanitized === 'auto') { return sanitized; } return null; } // Validate file upload static validateAudioFile(file: File): { valid: boolean; error?: string } { // Check if file exists if (!file) { return { valid: false, error: 'No file provided' }; } // Check file size (max 25MB) const maxSize = 25 * 1024 * 1024; if (file.size > maxSize) { return { valid: false, error: 'File size exceeds 25MB limit' }; } // Check file type const allowedTypes = [ 'audio/webm', 'audio/ogg', 'audio/wav', 'audio/mp3', 'audio/mpeg', 'audio/mp4', 'audio/x-m4a', 'audio/x-wav' ]; if (!allowedTypes.includes(file.type)) { // Check by extension as fallback const ext = file.name.toLowerCase().split('.').pop(); const allowedExtensions = ['webm', 'ogg', 'wav', 'mp3', 'mp4', 'm4a']; if (!ext || !allowedExtensions.includes(ext)) { return { valid: false, error: 'Invalid audio file type' }; } } return { valid: true }; } // Validate URL static validateURL(url: string): string | null { if (!url || typeof url !== 'string') { return null; } try { const parsed = new URL(url); // Only allow http and https if (!['http:', 'https:'].includes(parsed.protocol)) { return null; } // Prevent localhost in production if (window.location.hostname !== 'localhost' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')) { return null; } return parsed.toString(); } catch (e) { return null; } } // Validate API key (basic format check) static validateAPIKey(key: string): string | null { if (!key || typeof key !== 'string') { return null; } // Trim whitespace const trimmed = key.trim(); // Check length (most API keys are 20-128 characters) if (trimmed.length < 20 || trimmed.length > 128) { return null; } // Only allow alphanumeric, dash, and underscore if (!/^[a-zA-Z0-9\-_]+$/.test(trimmed)) { return null; } return trimmed; } // Validate request body size static validateRequestSize(data: any, maxSizeKB: number = 1024): boolean { try { const jsonString = JSON.stringify(data); const sizeInBytes = new Blob([jsonString]).size; return sizeInBytes <= maxSizeKB * 1024; } catch (e) { return false; } } // Sanitize filename static sanitizeFilename(filename: string): string { if (!filename || typeof filename !== 'string') { return 'file'; } // Remove path components let name = filename.split(/[/\\]/).pop() || 'file'; // Remove dangerous characters name = name.replace(/[^a-zA-Z0-9.\-_]/g, '_'); // Limit length if (name.length > 255) { const ext = name.split('.').pop(); const base = name.substring(0, 250 - (ext ? ext.length + 1 : 0)); name = ext ? `${base}.${ext}` : base; } return name; } // Validate settings object static validateSettings(settings: any): { valid: boolean; sanitized?: any; errors?: string[] } { const errors: string[] = []; const sanitized: any = {}; // Validate notification settings if (settings.notificationsEnabled !== undefined) { sanitized.notificationsEnabled = Boolean(settings.notificationsEnabled); } if (settings.notifyTranscription !== undefined) { sanitized.notifyTranscription = Boolean(settings.notifyTranscription); } if (settings.notifyTranslation !== undefined) { sanitized.notifyTranslation = Boolean(settings.notifyTranslation); } if (settings.notifyErrors !== undefined) { sanitized.notifyErrors = Boolean(settings.notifyErrors); } // Validate offline mode if (settings.offlineMode !== undefined) { sanitized.offlineMode = Boolean(settings.offlineMode); } // Validate TTS settings if (settings.ttsServerUrl !== undefined) { const url = this.validateURL(settings.ttsServerUrl); if (settings.ttsServerUrl && !url) { errors.push('Invalid TTS server URL'); } else { sanitized.ttsServerUrl = url; } } if (settings.ttsApiKey !== undefined) { const key = this.validateAPIKey(settings.ttsApiKey); if (settings.ttsApiKey && !key) { errors.push('Invalid API key format'); } else { sanitized.ttsApiKey = key; } } return { valid: errors.length === 0, sanitized: errors.length === 0 ? sanitized : undefined, errors: errors.length > 0 ? errors : undefined }; } // Rate limiting check private static requestCounts: Map = new Map(); static checkRateLimit( action: string, maxRequests: number = 10, windowMs: number = 60000 ): boolean { const now = Date.now(); const key = action; if (!this.requestCounts.has(key)) { this.requestCounts.set(key, []); } const timestamps = this.requestCounts.get(key)!; // Remove old timestamps const cutoff = now - windowMs; const recent = timestamps.filter(t => t > cutoff); // Check if limit exceeded if (recent.length >= maxRequests) { return false; } // Add current timestamp recent.push(now); this.requestCounts.set(key, recent); return true; } // Validate translation cache key static sanitizeCacheKey(key: string): string { if (!key || typeof key !== 'string') { return ''; } // Remove special characters that might cause issues return key.replace(/[^\w\s-]/gi, '').substring(0, 500); } }