**Motivations :** - Fix WebSocket message validation for very large handshake messages (11MB+) - Allow processing of extensive peer lists in handshake content **Modifications :** - Increased maxMessageSize from 1MB to 50MB - Maintained security while allowing large legitimate messages **Pages affectées :** - src/services/message-validator.ts - Increased message size limit
356 lines
9.6 KiB
TypeScript
356 lines
9.6 KiB
TypeScript
/**
|
|
* 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();
|