Экранирование HTML

HTML в интерфейсах выбора данных становится источником уязвимостей тогда, когда библиотека или разработчик начинают подставлять пользовательский ввод напрямую в innerHTML без экранирования. В контексте Tom Select это особенно критично, поскольку элементы списка, опции и создаваемые значения часто приходят из внешних источников: API, форм пользователя, импортированных наборов данных.

Tom Select по умолчанию включает защитный механизм экранирования HTML. Это означает, что строки, содержащие потенциально опасные конструкции вроде <script> или <img oner ror=...>, не интерпретируются как HTML, а отображаются как обычный текст.

Ключевое поведение задаётся параметром:

escape_html: true

Это значение включено по умолчанию и отвечает за преобразование специальных символов:

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

Таким образом, даже если данные приходят из ненадёжного источника, они не превращаются в DOM-структуру.

Внутренний механизм экранирования

Внутри Tom Select используется функция:

TomSelect.escape_html()

Она применяется ко всем строковым значениям, которые попадают в визуальные элементы списка: option, item, create-option и другие шаблоны отображения.

Пример поведения:

TomSelect.escape_html('<b>test</b>')
// &lt;b&gt;test&lt;/b&gt;

Эта функция используется в стандартных рендерах, если разработчик не переопределяет шаблоны вручную.

Рендеринг элементов и точки риска

Основной источник проблем с HTML-экранированием возникает при переопределении render:

new TomSelect('#select', {
  render: {
    option: function(data, escape) {
      return `<div>${data.text}</div>`;
    }
  }
});

В данном случае экранирование не применяется автоматически, если разработчик не использует переданный escape или TomSelect.escape_html.

Правильный вариант:

render: {
  option: function(data, escape) {
    return `<div>${escape(data.text)}</div>`;
  }
}

Именно пропуск экранирования внутри кастомных шаблонов является наиболее частой причиной XSS-уязвимостей.

Режим создания новых значений

При использовании возможности добавления новых элементов (create: true) пользовательский ввод становится частью данных модели:

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

Создаваемое значение также проходит через экранирование, если не переопределён рендер.

Однако риск возникает при кастомном отображении созданных элементов:

render: {
  item: function(data, escape) {
    return `<div>${data.text}</div>`;
  }
}

Здесь экранирование отсутствует, и значение может быть интерпретировано как HTML.

Корректная реализация:

render: {
  item: function(data, escape) {
    return `<div>${escape(data.text)}</div>`;
  }
}

Опции и optgroup как источник HTML-инъекций

Структуры данных для Tom Select часто приходят в виде объектов:

{
  value: "1",
  text: "Example"
}

или групп:

{
  optgroup: "Group <b>name</b>"
}

Если optgroup или text содержат HTML и не экранируются при кастомном рендере, они могут быть вставлены в DOM без обработки.

Особенно опасны сценарии:

  • загрузка данных через load() из внешнего API
  • серверный рендер JSON без очистки
  • локализация, где строки приходят из CMS

Загрузка данных через AJAX

При использовании динамической загрузки:

load: function(query, callback) {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(data => callback(data));
}

Tom Select не гарантирует санитарную очистку содержимого ответа. Он полагается на экранирование при отображении.

Если сервер возвращает:

{
  "text": "<img src=x oner ror=alert(1)>"
}

то защита работает только при условии, что escape_html не отключён и кастомные рендеры используют escape.

Отключение экранирования и его последствия

Параметр:

escape_html: false

полностью отключает автоматическое преобразование HTML-символов. В этом режиме Tom Select начинает интерпретировать строки как потенциальный HTML.

Использование этого режима допустимо только при строгой гарантии, что:

  • все данные предварительно очищены на сервере
  • запрещены пользовательские HTML-теги
  • рендеры контролируются и не используют прямой innerHTML

Иначе любой внешний ввод превращается в вектор XSS.

Безопасные шаблоны рендера

Рекомендуемая модель построения UI-элементов:

render: {
  option: function(data, escape) {
    return `
      <div class="option">
        <span class="label">${escape(data.text)}</span>
      </div>
    `;
  }
}

или с использованием утилиты библиотеки:

TomSelect.escape_html(data.text)

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

Разделение данных и HTML-структуры

Ключевой принцип безопасной работы с Tom Select заключается в разделении:

  • данные (value, text, metadata)
  • представление (render functions)

Нельзя смешивать HTML-разметку и данные:

// опасный подход
text: "<b>Admin</b>"

Правильнее:

text: "Admin",
highlight: true

и уже в render решать, как отображать выделение.

Атрибуты data-* и безопасное хранение

Даже при использовании data-* атрибутов необходимо учитывать, что они могут попасть в DOM:

render: {
  option: function(data, escape) {
    return `<div data-id="${escape(data.value)}">${escape(data.text)}</div>`;
  }
}

Любая подстановка без экранирования может привести к разрыву атрибутов и внедрению HTML.

Итоговая модель защиты в Tom Select

Безопасное использование экранирования строится на трёх уровнях:

  1. Автоматическое экранирование (escape_html: true)
  2. Явное использование escape() в render-функциях
  3. Санитизация данных на сервере до попадания в клиент

Нарушение любого уровня создаёт потенциальную точку внедрения HTML в DOM, особенно при кастомных шаблонах и загрузке внешних данных.