Валидация входящих данных

В системах обмена сообщениями ошибка в структуре сообщения способна привести к цепочке проблем:

  • повреждение состояния приложения;
  • выполнение некорректной бизнес-логики;
  • падение обработчиков;
  • переполнение очередей;
  • уязвимости безопасности;
  • рассинхронизация между клиентами;
  • утечки памяти из-за бесконтрольных данных.

При работе со STOMP.js особенно важно валидировать данные, поступающие через подписки (SUBSCRIBE) и пользовательские каналы. WebSocket-соединение поддерживает постоянный поток сообщений, поэтому ошибка в одном пакете способна многократно повторяться и вызывать нестабильность всей системы.

Валидация должна выполняться:

  • до обработки полезной нагрузки;
  • до преобразования типов;
  • до сохранения данных;
  • до передачи данных другим модулям;
  • до обновления интерфейса.

Источники входящих данных

В STOMP.js входящие данные обычно поступают через callback-функции подписок:

client.subscribe('/topic/orders', (message) => {
    console.log(message.body);
});

Источниками потенциально некорректных данных являются:

  • другие клиенты;
  • сторонние сервисы;
  • брокеры сообщений;
  • промежуточные прокси;
  • устаревшие версии API;
  • повреждённые JSON-пакеты;
  • вредоносные сообщения.

Даже если сервер считается доверенным, клиентская проверка остаётся обязательной.


Проверка наличия данных

Первичная валидация начинается с проверки самого сообщения.

client.subscribe('/topic/orders', (message) => {

    if (!message) {
        return;
    }

    if (!message.body) {
        return;
    }

    console.log(message.body);
});

Без подобной проверки возможны ошибки:

Cannot read properties of undefined

Особенно это важно при реконнектах и нестабильных соединениях.


Валидация JSON

Большинство STOMP-приложений передаёт JSON.

Нельзя напрямую вызывать JSON.parse() без обработки исключений.

Небезопасный вариант:

const data = JSON.parse(message.body);

Безопасный вариант:

function safeParseJSON(text) {

    try {
        return JSON.parse(text);
    } catch (error) {
        return null;
    }

}

client.subscribe('/topic/orders', (message) => {

    const data = safeParseJSON(message.body);

    if (!data) {
        console.error('Некорректный JSON');
        return;
    }

    console.log(data);

});

Проверка структуры объекта

После успешного парсинга необходимо валидировать структуру объекта.

Пример ожидаемого сообщения:

{
    "id": 15,
    "status": "created",
    "amount": 1200
}

Проверка:

function validateOrder(data) {

    if (typeof data !== 'object') {
        return false;
    }

    if (typeof data.id !== 'number') {
        return false;
    }

    if (typeof data.status !== 'string') {
        return false;
    }

    if (typeof data.amount !== 'number') {
        return false;
    }

    return true;

}

Использование:

client.subscribe('/topic/orders', (message) => {

    const data = safeParseJSON(message.body);

    if (!validateOrder(data)) {
        console.error('Ошибка структуры данных');
        return;
    }

    processOrder(data);

});

Проверка обязательных полей

Даже корректный JSON может не содержать нужных полей.

Проблемное сообщение:

{
    "status": "created"
}

Проверка обязательных свойств:

function hasRequiredFields(data, fields) {

    return fields.every(field => field in data);

}

Использование:

if (!hasRequiredFields(data, ['id', 'status', 'amount'])) {
    console.error('Отсутствуют обязательные поля');
    return;
}

Проверка типов данных

В JavaScript типы могут приходить в неожиданном виде:

{
    "id": "15",
    "amount": "500"
}

Если логика ожидает числа, необходимо явно проверять типы.

function isNumber(value) {
    return typeof value === 'number' && !isNaN(value);
}

Проверка:

if (!isNumber(data.amount)) {
    console.error('amount должен быть числом');
    return;
}

Проверка диапазонов значений

Даже корректный тип не гарантирует корректное значение.

Некорректные примеры:

{
    "amount": -100000
}
{
    "amount": 999999999999999999
}

Проверка диапазона:

function validateAmount(amount) {

    return amount >= 0 && amount <= 1000000;

}

Проверка строковых значений

Строки могут содержать:

  • пустые значения;
  • SQL-инъекции;
  • HTML;
  • JavaScript-код;
  • слишком длинные данные;
  • бинарный мусор.

Проверка:

function validateStatus(status) {

    const allowed = [
        'created',
        'paid',
        'cancelled'
    ];

    return allowed.includes(status);

}

Ограничение размера сообщений

Одна из важных мер безопасности — ограничение размера входящего сообщения.

Опасный сценарий:

50 МБ JSON через WebSocket

Подобное сообщение способно:

  • заморозить интерфейс;
  • вызвать переполнение памяти;
  • заблокировать event loop.

Проверка:

const MAX_MESSAGE_SIZE = 1024 * 100;

client.subscribe('/topic/orders', (message) => {

    if (message.body.length > MAX_MESSAGE_SIZE) {
        console.error('Сообщение слишком большое');
        return;
    }

});

Проверка MIME-типа

STOMP-сообщения поддерживают заголовки.

Например:

client.publish({
    destination: '/topic/orders',
    headers: {
        'content-type': 'application/json'
    },
    body: JSON.stringify(data)
});

Проверка на принимающей стороне:

client.subscribe('/topic/orders', (message) => {

    const contentType = message.headers['content-type'];

    if (contentType !== 'application/json') {
        console.error('Неверный content-type');
        return;
    }

});

Валидация enum-значений

Часто сообщения содержат фиксированные состояния.

Пример:

{
    "status": "unknown_status"
}

Проверка:

const allowedStatuses = new Set([
    'created',
    'processing',
    'done',
    'failed'
]);

if (!allowedStatuses.has(data.status)) {
    console.error('Недопустимый статус');
    return;
}

Валидация временных меток

Timestamp-поля должны проверяться отдельно.

Пример:

{
    "createdAt": "invalid-date"
}

Проверка:

function isValidDate(date) {

    return !isNaN(Date.parse(date));

}

Защита от XSS

STOMP.js часто используется в чатах и realtime-интерфейсах.

Опасный пример:

{
    "message": "<script>alert(1)</script>"
}

Нельзя вставлять входящие данные напрямую через innerHTML.

Опасно:

container.innerHTML = data.message;

Безопаснее:

container.textContent = data.message;

Санитизация HTML

Если HTML всё же необходим, требуется очистка.

Пример с DOMPurify:

import DOMPurify from 'dompurify';

const cleanHTML = DOMPurify.sanitize(data.message);

Использование схем валидации

В крупных приложениях ручные проверки становятся неудобными.

Популярные библиотеки:

  • Yup;
  • Joi;
  • Zod;
  • Ajv;
  • Superstruct.

Валидация через Zod

Пример схемы:

import { z } from 'zod';

const OrderSchema = z.object({
    id: z.number(),
    status: z.string(),
    amount: z.number().min(0)
});

Проверка:

client.subscribe('/topic/orders', (message) => {

    const data = safeParseJSON(message.body);

    const result = OrderSchema.safeParse(data);

    if (!result.success) {
        console.error(result.error);
        return;
    }

    processOrder(result.data);

});

Валидация через Ajv

Ajv особенно полезен для JSON Schema.

Схема:

const schema = {
    type: 'object',
    properties: {
        id: { type: 'number' },
        status: { type: 'string' },
        amount: { type: 'number' }
    },
    required: ['id', 'status', 'amount']
};

Проверка:

import Ajv from 'ajv';

const ajv = new Ajv();

const validate = ajv.compile(schema);

if (!validate(data)) {
    console.error(validate.errors);
}

Валидация массивов

Сообщения часто содержат списки объектов.

Пример:

{
    "items": [
        {
            "id": 1
        }
    ]
}

Проверка:

if (!Array.isArray(data.items)) {
    return;
}

Валидация элементов:

for (const item of data.items) {

    if (typeof item.id !== 'number') {
        return;
    }

}

Проверка вложенных объектов

Глубоко вложенные структуры требуют отдельной проверки.

Пример:

{
    "user": {
        "profile": {
            "name": "Alex"
        }
    }
}

Безопасная проверка:

if (
    !data.user ||
    !data.user.profile ||
    typeof data.user.profile.name !== 'string'
) {
    return;
}

Защита от prototype pollution

Вредоносный JSON может содержать:

{
    "__proto__": {
        "admin": true
    }
}

Проблема особенно опасна при merge-операциях.

Небезопасно:

Object.assign(target, data);

Защита:

function hasDangerousKeys(obj) {

    return [
        '__proto__',
        'constructor',
        'prototype'
    ].some(key => key in obj);

}

Нормализация данных

После проверки данные часто приводятся к единому формату.

Пример:

function normalizeOrder(data) {

    return {
        id: Number(data.id),
        status: data.status.trim(),
        amount: Number(data.amount)
    };

}

Валидация перед ACK

При использовании клиентских подтверждений (client-individual) особенно важно валидировать данные до ACK.

client.subscribe('/queue/orders', (message) => {

    const data = safeParseJSON(message.body);

    if (!validateOrder(data)) {

        console.error('Некорректное сообщение');

        return;
    }

    processOrder(data);

    message.ack();

}, {
    ack: 'client-individual'
});

Если ACK отправить раньше проверки, брокер удалит сообщение даже при ошибке обработки.


Повторная обработка некорректных сообщений

Некоторые системы используют retry-механизмы.

Пример:

let retries = 0;

function processMessage(message) {

    try {

        const data = JSON.parse(message.body);

        validateOrder(data);

    } catch (error) {

        retries++;

        if (retries < 3) {
            setTimeout(() => {
                processMessage(message);
            }, 1000);
        }

    }

}

Изоляция ошибок обработчика

Ошибка в одном callback не должна ломать всё соединение.

Небезопасно:

client.subscribe('/topic/orders', (message) => {

    const data = JSON.parse(message.body);

    processOrder(data);

});

Безопаснее:

client.subscribe('/topic/orders', (message) => {

    try {

        const data = JSON.parse(message.body);

        processOrder(data);

    } catch (error) {

        console.error(error);

    }

});

Логирование ошибок валидации

Полезно фиксировать:

  • тип ошибки;
  • время;
  • идентификатор сообщения;
  • размер payload;
  • stack trace;
  • пользователя;
  • канал подписки.

Пример:

function logValidationError(error, message) {

    console.error({
        error,
        subscription: message.headers.destination,
        messageId: message.headers['message-id'],
        timestamp: Date.now()
    });

}

Отбрасывание подозрительных сообщений

В некоторых случаях сообщение лучше полностью игнорировать.

Признаки подозрительности:

  • слишком большой payload;
  • неизвестный тип события;
  • отсутствие обязательных полей;
  • аномальная вложенность;
  • бинарные данные вместо текста;
  • подозрительные заголовки;
  • слишком частые сообщения.

Rate limiting входящих сообщений

Даже валидные сообщения могут перегружать приложение.

Пример ограничения:

let counter = 0;

setInterval(() => {
    counter = 0;
}, 1000);

client.subscribe('/topic/events', (message) => {

    counter++;

    if (counter > 100) {
        return;
    }

    processEvent(message);

});

Централизованная система валидации

В больших проектах удобно создавать единый валидатор.

Пример архитектуры:

class MessageValidator {

    validate(message) {

        const data = this.parse(message.body);

        this.validateSchema(data);

        this.validateSecurity(data);

        return data;

    }

    parse(body) {
        return JSON.parse(body);
    }

    validateSchema(data) {

    }

    validateSecurity(data) {

    }

}

Использование:

const validator = new MessageValidator();

client.subscribe('/topic/orders', (message) => {

    try {

        const data = validator.validate(message);

        processOrder(data);

    } catch (error) {

        console.error(error);

    }

});

Валидация бинарных сообщений

Некоторые брокеры могут передавать бинарные payload.

Проверка:

if (typeof message.body !== 'string') {
    return;
}

Валидация событийной модели

Полезно проверять тип события.

Пример:

{
    "event": "user_created"
}

Проверка:

const allowedEvents = [
    'user_created',
    'user_deleted',
    'user_updated'
];

if (!allowedEvents.includes(data.event)) {
    return;
}

Проверка последовательности сообщений

Realtime-системы иногда требуют контроля порядка.

Пример:

{
    "sequence": 150
}

Проверка:

if (data.sequence <= lastSequence) {
    return;
}

lastSequence = data.sequence;

Валидация UUID

Проверка идентификаторов:

function isUUID(value) {

    const regex =
        /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

    return regex.test(value);

}

Проверка URL

Пример:

function isValidURL(value) {

    try {

        new URL(value);

        return true;

    } catch {

        return false;

    }

}

Асинхронная валидация

Иногда требуется серверная проверка.

Пример:

async function validateUser(userId) {

    const response = await fetch(`/api/users/${userId}`);

    return response.ok;

}

Использование:

client.subscribe('/topic/orders', async (message) => {

    const data = JSON.parse(message.body);

    const exists = await validateUser(data.userId);

    if (!exists) {
        return;
    }

});