Валидация на сервере

Choices.js работает на клиентской стороне и отвечает за удобное управление выпадающими списками, мультиселектами и вводом значений. Любые значения, выбранные через интерфейс, по своей природе являются недоверенными данными, поскольку могут быть изменены пользователем до отправки запроса. Серверная валидация в таких сценариях становится обязательным уровнем защиты, который полностью определяет, какие значения считаются допустимыми независимо от поведения UI.

Любой компонент выбора значений, даже визуально ограниченный, не гарантирует целостность данных:

  • DOM может быть изменён вручную
  • HTTP-запрос может быть подменён
  • JavaScript может быть отключён или переписан
  • API может быть вызван напрямую без UI

В случае Choices.js это особенно критично, поскольку библиотека лишь формирует интерфейс, но не накладывает обязательных ограничений на серверную часть.

Основные классы угроз:

1. Подмена значений Пользователь может отправить значения, отсутствующие в списке choices, включая скрытые или удалённые элементы.

2. Инъекция бизнес-логики Передача невалидных идентификаторов, приводящая к изменению состояния системы (например, доступ к чужим ресурсам).

3. Массовая подмена payload Отправка массивов значений, превышающих допустимые ограничения интерфейса.

4. Обход ограничений UI Игнорирование disabled/hidden элементов через прямые HTTP-запросы.

Принцип доверия: сервер как единственный источник истины

Ключевая архитектурная идея заключается в том, что клиент никогда не является источником истины. Любое значение, поступающее от Choices.js, рассматривается как потенциально поддельное.

Сервер должен:

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

Базовая схема валидации

Типичный поток данных:

  1. Сервер отдает список допустимых значений
  2. Клиент отображает их через Choices.js
  3. Пользователь выбирает элементы
  4. Клиент отправляет массив value
  5. Сервер повторно проверяет каждый элемент

Пример структуры запроса:

{
  "categories": ["tech", "science", "music"]
}

Whitelist как основной механизм защиты

Самый надёжный подход — использование белого списка значений.

const allowedCategories = new Set([
  "tech",
  "science",
  "music",
  "sports"
]);

function validateCategories(input) {
  if (!Array.isArray(input)) {
    return false;
  }

  return input.every(v => allowedCategories.has(v));
}

Даже если Choices.js на клиенте отображает другие данные, сервер не примет их.

Связь с источником данных

На практике список choices почти всегда приходит из базы данных:

async function getAllowedCategoriesFromDB() {
  return db.query("SEL ECT slug FR OM categories WHERE active = true");
}

Валидация должна использовать тот же источник:

async function validateCategories(input) {
  const rows = await getAllowedCategoriesFromDB();
  const allowed = new Set(rows.map(r => r.slug));

  return input.every(v => allowed.has(v));
}

Валидация одиночных и множественных значений

Choices.js часто используется в двух режимах: single sel ect и multi select.

Одиночное значение

function validateSingle(value, allowedSet) {
  return typeof value === "string" && allowedSet.has(value);
}

Множественные значения

function validateMultiple(values, allowedSet) {
  if (!Array.isArray(values)) return false;

  return values.length > 0 && values.every(v => allowedSet.has(v));
}

Дополнительно часто вводятся ограничения:

  • максимальная длина массива
  • уникальность элементов
  • запрет пустых значений

Нормализация данных перед проверкой

Перед валидацией входные данные приводятся к каноническому виду:

function normalize(input) {
  if (typeof input === "string") {
    return input.trim();
  }

  if (Array.isArray(input)) {
    return input.map(v => v.trim()).filter(Boolean);
  }

  return input;
}

Нормализация особенно важна при интеграции с UI, где Choices.js может возвращать значения с неожиданным форматированием (например, пробелы или строковые числа).

Проверка типов и схемы

Для сложных API применяется схема валидации:

const schema = {
  categories: {
    type: "array",
    maxItems: 5,
    items: { type: "string" }
  }
};

Пример ручной проверки:

function validateSchema(data) {
  if (!Array.isArray(data.categories)) return false;
  if (data.categories.length > 5) return false;

  return data.categories.every(v => typeof v === "string");
}

Асинхронная валидация через базу данных

Когда choices зависят от состояния системы, требуется асинхронная проверка:

async function validateUserTags(userId, tags) {
  const allowed = await db.query(
    "SELECT tag FR OM user_tags WHERE user_id = ?",
    [userId]
  );

  const allowedSet = new Set(allowed.map(t => t.tag));

  return tags.every(tag => allowedSet.has(tag));
}

Такая модель исключает возможность использования устаревшего списка, который мог быть закеширован на клиенте Choices.js.

Защита от массовых атак и перегрузки

Серверная валидация должна учитывать не только корректность, но и нагрузку:

  • ограничение размера массива
  • ограничение длины строк
  • отказ от глубокой вложенности
const MAX_ITEMS = 20;

function safeValidate(values) {
  if (values.length > MAX_ITEMS) {
    return false;
  }
  return true;
}

Интеграция с бизнес-правилами

Валидация choices часто зависит от контекста:

  • роль пользователя
  • состояние объекта
  • региональные ограничения
function validateByRole(user, values) {
  const allowed = user.role === "admin"
    ? getAdminChoices()
    : getUserChoices();

  const allowedSet = new Set(allowed);

  return values.every(v => allowedSet.has(v));
}

Обработка ошибок валидации

Сервер должен возвращать структурированные ошибки:

{
  "error": "VALIDATION_ERROR",
  "fields": {
    "categories": "contains invalid values"
  }
}

Подробная диагностика важна для синхронизации состояния между UI Choices.js и сервером.

Согласованность состояния между клиентом и сервером

Распространённая проблема — расхождение данных:

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

Решение:

  • версии списков (ETag / version field)
  • повторная синхронизация перед сохранением
  • отказ от доверия клиентскому cache

Использование идентификаторов вместо строк

Надёжнее использовать ID:

{
  "categories": [12, 15, 18]
}

В этом случае сервер проверяет:

function validateIds(ids, allowedIds) {
  const set = new Set(allowedIds);
  return ids.every(id => set.has(id));
}

Логирование и аудит

Любые отклонённые значения должны фиксироваться:

function logInvalidInput(userId, payload) {
  logger.warn("Invalid choices payload", {
    userId,
    payload,
    timestamp: Date.now()
  });
}

Это помогает выявлять:

  • попытки обхода UI Choices.js
  • ошибки клиента
  • неконсистентность данных

Защита от injection через значения choices

Даже если значения выглядят безопасно, они могут использоваться в:

  • SQL-запросах
  • фильтрах ORM
  • поисковых индексах

Поэтому после валидации обязательна параметризация:

db.query("SEL ECT * FR OM items WHERE category IN (?)", [validatedValues]);

Контракт API и стабильность данных

Серверный контракт должен явно определять:

  • формат choices
  • допустимые типы
  • ограничения по размеру
  • правила актуализации

Choices.js в таком контексте рассматривается только как слой отображения, не влияющий на правила.

Итоговая модель обработки данных

Типовой pipeline:

  1. получение payload
  2. нормализация
  3. проверка типа
  4. проверка размера
  5. сверка с источником истины
  6. бизнес-валидация
  7. логирование
  8. формирование ответа

Каждый шаг обязателен, даже если интерфейс уже ограничивает выбор.