/** * MessageValidator - Validation et sanitisation des messages */ export interface ValidationResult { isValid: boolean; errors: string[]; sanitizedData?: any; } export interface WebSocketMessage { flag: string; content: any; timestamp?: number; id?: string; } export class MessageValidator { private static instance: MessageValidator; private maxMessageSize = 50 * 1024 * 1024; // 50MB for large handshake messages private maxStringLength = 100000; // Increased for large handshake messages private allowedFlags = ['Handshake', 'NewTx', 'Cipher', 'Commit']; private constructor() {} public static getInstance(): MessageValidator { if (!MessageValidator.instance) { MessageValidator.instance = new MessageValidator(); } return MessageValidator.instance; } /** * Valide un message WebSocket */ validateWebSocketMessage(data: any): ValidationResult { const errors: string[] = []; // Vérifier le type de données if (typeof data !== 'string') { errors.push('Message must be a string'); return { isValid: false, errors }; } // Vérifier la taille du message if (data.length > this.maxMessageSize) { errors.push(`Message too large: ${data.length} bytes (max: ${this.maxMessageSize})`); return { isValid: false, errors }; } // Parser le JSON let parsedMessage: any; try { parsedMessage = JSON.parse(data); } catch (error) { errors.push('Invalid JSON format'); return { isValid: false, errors }; } // Valider la structure du message const structureValidation = this.validateMessageStructure(parsedMessage); if (!structureValidation.isValid) { return structureValidation; } // Sanitiser le contenu const sanitizedContent = this.sanitizeContent(parsedMessage.content); return { isValid: true, errors: [], sanitizedData: { ...parsedMessage, content: sanitizedContent, timestamp: Date.now() } }; } /** * Valide la structure d'un message */ private validateMessageStructure(message: any): ValidationResult { const errors: string[] = []; // Vérifier que c'est un objet if (typeof message !== 'object' || message === null) { errors.push('Message must be an object'); return { isValid: false, errors }; } // Vérifier la présence du flag if (!message.flag || typeof message.flag !== 'string') { errors.push('Message must have a valid flag'); return { isValid: false, errors }; } // Vérifier que le flag est autorisé if (!this.allowedFlags.includes(message.flag)) { errors.push(`Invalid flag: ${message.flag}. Allowed: ${this.allowedFlags.join(', ')}`); return { isValid: false, errors }; } // Vérifier la présence du contenu if (message.content === undefined || message.content === null) { errors.push('Message must have content'); return { isValid: false, errors }; } // Vérifier le type du contenu - peut être un objet ou une string JSON if (typeof message.content === 'string') { // Parser le contenu JSON si c'est une string try { message.content = JSON.parse(message.content); } catch (error) { errors.push('Content must be valid JSON if it is a string'); return { isValid: false, errors }; } } else if (typeof message.content !== 'object') { errors.push('Content must be an object or valid JSON string'); return { isValid: false, errors }; } return { isValid: true, errors: [] }; } /** * Sanitise le contenu d'un message */ private sanitizeContent(content: any): any { if (typeof content !== 'object' || content === null) { return content; } const sanitized: any = {}; for (const [key, value] of Object.entries(content)) { // Vérifier la longueur des clés if (key.length > this.maxStringLength) { continue; // Ignorer les clés trop longues } // Sanitiser les valeurs if (typeof value === 'string') { sanitized[key] = this.sanitizeString(value); } else if (typeof value === 'object' && value !== null) { sanitized[key] = this.sanitizeContent(value); } else { sanitized[key] = value; } } return sanitized; } /** * Sanitise une chaîne de caractères */ private sanitizeString(str: string): string { // Vérifier la longueur if (str.length > this.maxStringLength) { return str.substring(0, this.maxStringLength) + '...'; } // Supprimer les caractères de contrôle dangereux return str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); } /** * Valide un message de pairing */ validatePairingMessage(message: any): ValidationResult { const errors: string[] = []; if (!message.type) { errors.push('Pairing message must have a type'); return { isValid: false, errors }; } const allowedTypes = [ 'PAIRING_4WORDS_CREATE', 'PAIRING_4WORDS_JOIN', 'PAIRING_4WORDS_WORDS_GENERATED', 'PAIRING_4WORDS_STATUS_UPDATE', 'PAIRING_4WORDS_SUCCESS', 'PAIRING_4WORDS_ERROR' ]; if (!allowedTypes.includes(message.type)) { errors.push(`Invalid pairing message type: ${message.type}`); return { isValid: false, errors }; } // Validation spécifique selon le type switch (message.type) { case 'PAIRING_4WORDS_JOIN': if (!message.words || typeof message.words !== 'string') { errors.push('PAIRING_4WORDS_JOIN requires words'); } if (message.words && message.words.length > 100) { errors.push('Words too long'); } break; } return { isValid: errors.length === 0, errors }; } /** * Valide un message de processus */ validateProcessMessage(message: any): ValidationResult { const errors: string[] = []; if (!message.type) { errors.push('Process message must have a type'); return { isValid: false, errors }; } const allowedTypes = [ 'CREATE_PROCESS', 'UPDATE_PROCESS', 'GET_PROCESSES', 'GET_MY_PROCESSES' ]; if (!allowedTypes.includes(message.type)) { errors.push(`Invalid process message type: ${message.type}`); return { isValid: false, errors }; } return { isValid: errors.length === 0, errors }; } /** * Valide un message de données */ validateDataMessage(message: any): ValidationResult { const errors: string[] = []; if (!message.type) { errors.push('Data message must have a type'); return { isValid: false, errors }; } const allowedTypes = [ 'RETRIEVE_DATA', 'STORE_DATA', 'DELETE_DATA' ]; if (!allowedTypes.includes(message.type)) { errors.push(`Invalid data message type: ${message.type}`); return { isValid: false, errors }; } // Vérifier la présence des données requises if (message.type === 'STORE_DATA' && !message.data) { errors.push('STORE_DATA requires data'); } if (message.type === 'RETRIEVE_DATA' && !message.key) { errors.push('RETRIEVE_DATA requires key'); } return { isValid: errors.length === 0, errors }; } /** * Valide un message de token */ validateTokenMessage(message: any): ValidationResult { const errors: string[] = []; if (!message.type) { errors.push('Token message must have a type'); return { isValid: false, errors }; } const allowedTypes = [ 'VALIDATE_TOKEN', 'RENEW_TOKEN', 'REVOKE_TOKEN' ]; if (!allowedTypes.includes(message.type)) { errors.push(`Invalid token message type: ${message.type}`); return { isValid: false, errors }; } // Vérifier la présence du token if (!message.token) { errors.push('Token message requires token'); return { isValid: false, errors }; } // Valider le format du token if (typeof message.token !== 'string' || message.token.length < 10) { errors.push('Invalid token format'); return { isValid: false, errors }; } return { isValid: errors.length === 0, errors }; } /** * Valide un message générique */ validateGenericMessage(message: any): ValidationResult { const errors: string[] = []; if (!message.type) { errors.push('Message must have a type'); return { isValid: false, errors }; } // Vérifier la longueur du type if (typeof message.type !== 'string' || message.type.length > 100) { errors.push('Invalid message type'); return { isValid: false, errors }; } // Vérifier la présence de données si nécessaire if (message.data && typeof message.data !== 'object') { errors.push('Message data must be an object'); return { isValid: false, errors }; } return { isValid: errors.length === 0, errors }; } /** * Valide un message en fonction de son type */ validateMessage(message: any): ValidationResult { if (!message.type) { return { isValid: false, errors: ['Message must have a type'] }; } // Validation spécifique selon le type if (message.type.startsWith('PAIRING_')) { return this.validatePairingMessage(message); } else if (message.type.includes('PROCESS')) { return this.validateProcessMessage(message); } else if (message.type.includes('DATA')) { return this.validateDataMessage(message); } else if (message.type.includes('TOKEN')) { return this.validateTokenMessage(message); } else { return this.validateGenericMessage(message); } } } // Instance singleton pour l'application export const messageValidator = MessageValidator.getInstance();