Санитизация пользовательского ввода

Общая природа пользовательского ввода и риски

Любой компонент, который принимает текст от пользователя и отображает его в DOM, становится потенциальной точкой внедрения вредоносного кода. В контексте Tom Select, где элементы списка, теги и опции часто формируются динамически, основная угроза связана с внедрением HTML и JavaScript через неподготовленные строки.

Типичные векторы атак включают:

  • внедрение <script> через значения опций;
  • использование HTML-атрибутов с обработчиками событий (onerror, onload);
  • подмена отображаемого текста через несбалансированные HTML-теги;
  • инъекция через кастомные шаблоны рендера.

Tom Select по умолчанию минимизирует риск, но не исключает его полностью, особенно при использовании кастомных рендеров (render) и динамических источников данных (load).


Поведение Tom Select при рендеринге значений

Tom Select работает с двумя основными сущностями:

  • value — идентификатор элемента;
  • text — отображаемый текст.

При стандартной конфигурации библиотека экранирует текстовые значения при вставке в DOM, используя внутренние механизмы безопасного создания узлов. Однако это поведение может быть нарушено в следующих случаях:

  • использование render.option или render.item с возвратом HTML-строк;
  • ручная вставка HTML через innerHTML;
  • возврат неэкранированных строк из load callback.

Пример безопасного поведения:

new TomSelect("#select", {
  options: [
    { value: "1", text: "JavaScript" },
    { value: "2", text: "TypeScript" }
  ]
});

В этом случае библиотека сама создаёт текстовые узлы, предотвращая интерпретацию HTML.


Опасность кастомных рендеров

Наиболее частая причина уязвимостей — переопределение render.

Пример небезопасной реализации:

new TomSelect("#select", {
  render: {
    option: (data) => {
      return `<div class="option">${data.text}</div>`;
    }
  }
});

Если data.text содержит вредоносный код, он будет интерпретирован браузером как HTML:

data.text = `<img src=x oner ror=alert(1)>`;

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


Экранирование HTML как базовый механизм защиты

Минимально необходимый уровень защиты — экранирование специальных символов:

  • <&lt;
  • >&gt;
  • &&amp;
  • "&quot;
  • '&#039;

Базовая функция санитизации:

function escapeHTML(str) {
  return String(str)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

Применение в render:

new TomSelect("#select", {
  render: {
    option: (data) => {
      return `<div class="option">${escapeHTML(data.text)}</div>`;
    }
  }
});

Такой подход исключает интерпретацию HTML, превращая любой ввод в безопасный текстовый вывод.


Санитизация при загрузке данных (remote load)

При использовании асинхронной загрузки данных через load риск возрастает, так как источник часто внешний.

new TomSelect("#select", {
  load: function(query, callback) {
    fetch(`/api/search?q=${encodeURIComponent(query)}`)
      .then(res => res.json())
      .then(data => callback(data.items));
  }
});

Если API возвращает небезопасные строки, они попадут в интерфейс напрямую.

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

load: function(query, callback) {
  fetch(`/api/search?q=${encodeURIComponent(query)}`)
    .then(res => res.json())
    .then(data => {
      const safe = data.items.map(item => ({
        value: item.value,
        text: escapeHTML(item.text)
      }));
      callback(safe);
    });
}

Ключевой принцип: санитизация должна происходить до передачи данных в Tom Select, а не после рендеринга.


Разделение value и text как стратегия защиты

Правильная модель данных снижает риск XSS:

  • value никогда не должен содержать HTML;
  • text должен считаться недоверенным;
  • любые дополнительные поля (description, label, meta) также требуют обработки.

Пример корректной структуры:

{
  value: "user_123",
  text: "John Doe",
  role: "Administrator"
}

Даже при утечке text в HTML, значение value не должно использоваться для рендеринга без обработки.


Использование DocumentFragment вместо innerHTML

Безопасный способ генерации DOM-элементов в кастомных рендерах:

render: {
  option: (data) => {
    const div = document.createElement("div");
    div.className = "option";
    div.textContent = data.text;
    return div;
  }
}

textContent гарантирует, что содержимое будет интерпретировано только как текст, без HTML-разметки.

Этот подход полностью исключает XSS через рендеринг.


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

Tom Select часто используется для создания тегов (create: true), что делает ввод пользователя частью модели данных.

new TomSelect("#select", {
  create: true
});

В этом режиме пользователь может ввести произвольную строку, которая затем становится элементом списка.

Проблема возникает, если созданные значения отображаются через небезопасный render:

render: {
  item: (data) => `<div>${data.text}</div>`
}

Без экранирования это прямой путь к XSS.

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

render: {
  item: (data) => {
    const el = document.createElement("div");
    el.textContent = data.text;
    return el;
  }
}

Постобработка значений перед добавлением

Дополнительный уровень защиты — нормализация данных до вставки в Tom Select:

const sanitize = (input) => input.trim();

select.addOption({
  value: "tag",
  text: sanitize(userInput)
});

Для более строгой обработки может использоваться белый список символов:

function strictSanitize(str) {
  return str.replace(/[^\w\s-]/g, "");
}

Уязвимости при использовании HTML в шаблонах

Некоторые реализации намеренно используют HTML в render.option для кастомного интерфейса. Это допустимо только при строгом контроле источника данных.

Опасная практика:

return `<div>${data.label}</div>`;

Без гарантии происхождения data.label это приводит к внедрению HTML.

Безопасная альтернатива с частичным HTML:

const wrapper = document.createElement("div");

const title = document.createElement("span");
title.textContent = data.label;

wrapper.appendChild(title);
return wrapper;

Защита через CSP как дополнительный слой

Content Security Policy снижает последствия ошибок санитизации.

Пример политики:

Content-Security-Policy: default-src 'self'; script-src 'self'

Даже при наличии XSS-инъекции выполнение inline-script будет заблокировано.

Однако CSP не заменяет санитизацию, а лишь ограничивает ущерб.


Ошибки интеграции с фреймворками

При использовании Tom Select вместе с React, Vue или Angular часто возникает дублирование ответственности за рендеринг.

Проблема:

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

Типичный антипример:

const options = props.items.map(i => ({
  value: i.id,
  text: i.label
}));

Если i.label приходит из внешнего API без очистки, риск сохраняется даже при использовании фреймворка.


Итоговые принципы безопасной работы с данными

Безопасная интеграция Tom Select строится на нескольких уровнях защиты:

  • отказ от innerHTML в пользу textContent;
  • экранирование всех пользовательских строк;
  • санитизация данных до передачи в компонент;
  • контроль структуры value/text;
  • минимизация доверия к внешним API;
  • использование CSP как дополнительного барьера;
  • избегание HTML в render-функциях без строгого контроля источника.

Такая модель исключает большинство сценариев XSS и делает поведение компонента предсказуемым даже при работе с недоверенными данными.