XSS защита

Компоненты выбора с динамическими списками, такие как Tom Select, часто работают с пользовательскими или удалёнными данными, что создаёт прямую поверхность атаки для XSS (Cross-Site Scripting). Основная проблема возникает в момент, когда текст из источника данных без обработки попадает в DOM через innerHTML или аналогичные механизмы рендеринга.

Особенно уязвимыми становятся сценарии:

  • загрузка опций через AJAX;
  • создание новых элементов пользователем (create: true);
  • кастомный рендеринг опций (render.option, render.item);
  • отображение подсказок и описаний, приходящих с сервера.

Любая строка, содержащая HTML-разметку, потенциально превращается в исполняемый скрипт, если библиотека или пользовательский код вставляет её без экранирования.


Механика рендера в Tom Select и точка внедрения XSS

В Tom Select визуализация опций основана на функциях рендера:

  • render.option
  • render.item
  • render.option_create

Каждая из них возвращает HTML-строку или DOM-структуру, которая затем вставляется в список.

Уязвимость появляется, если используется прямое внедрение данных:

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

Если data.text содержит <script>alert(1)</script>, он выполнится при вставке в DOM.


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

Основной принцип защиты — исключение интерпретации HTML через экранирование специальных символов.

Минимальный набор символов:

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

В контексте Tom Select безопасный рендеринг должен использовать функцию экранирования:

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

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

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

Ключевой принцип: любой внешний ввод трактуется как текст, а не HTML.


Опасность allowHTML и HTML-данных из API

В Tom Select встречается сценарий, когда данные приходят с сервера уже в виде HTML:

{
  "text": "<b>Admin</b>"
}

Если включена логика отображения без экранирования, такой текст становится исполняемой разметкой.

Политика безопасности должна быть однозначной:

  • HTML из API считается недоверенным;
  • любые теги должны быть либо удалены, либо экранированы;
  • визуальное форматирование выполняется только на клиенте через шаблоны.

Безопасная кастомизация render-функций

При кастомном рендеринге важно избегать прямой конкатенации строк.

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

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

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

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

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


Защита при создании новых элементов (create:true)

Функция создания новых значений часто становится точкой XSS через пользовательский ввод:

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

Если пользователь вводит:

<script>alert(1)</script>

и это значение без обработки попадает в DOM, возникает уязвимость.

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

create: function(input) {
  return {
    value: input,
    text: input
  };
}

И обязательное экранирование при рендере:

render: {
  option_create: (data) => {
    return `<div>${escapeHTML(data.input)}</div>`;
  }
}

AJAX-источники данных и контроль входного потока

При подключении удалённых источников данных через AJAX:

  • данные считаются полностью недоверенными;
  • сервер не гарантирует отсутствие HTML/JS;
  • промежуточные прокси могут модифицировать ответы.

Рекомендуемая модель обработки:

  1. получать JSON;
  2. нормализовать поля;
  3. экранировать при рендере;
  4. не использовать HTML-строки из API напрямую.

Пример:

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

Использование DOM-API вместо HTML-строк

Наиболее надёжный подход в Tom Select — отказ от HTML-строк в пользу DOM-узлов:

render: {
  option: (data) => {
    const wrapper = document.createElement("div");
    const title = document.createElement("span");

    title.textContent = data.text;

    wrapper.appendChild(title);
    return wrapper;
  }
}

Преимущества:

  • невозможность инъекции скриптов;
  • отсутствие зависимости от экранирования;
  • предсказуемая структура DOM.

Санитизация через внешние библиотеки

В сложных случаях, когда требуется разрешённая HTML-разметка (например, жирный текст или подсветка совпадений), применяется санитизация через специализированные библиотеки.

Типичный подход:

  • разрешённый список тегов;
  • удаление событийных атрибутов (onclick, onerror);
  • блокировка inline-скриптов.

Пример с DOMPurify-подобной логикой:

const clean = sanitizeHTML(dirtyHTML);

render: {
  option: (data) => `<div>${clean(data.html)}</div>`
}

Важно: санитизация не заменяет экранирование, а является альтернативой только при необходимости HTML-рендеринга.


Защита пользовательского поиска и фильтрации

Функция поиска внутри списка также может использовать пользовательский ввод:

new TomSelect("#select", {
  searchField: ["text"]
});

Хотя поиск не вставляет HTML напрямую, он может:

  • влиять на отображаемые строки;
  • использовать шаблоны подсветки;
  • возвращать HTML из render.

Поэтому результат поиска должен проходить те же правила:

  • экранирование;
  • отказ от innerHTML;
  • контроль форматирования подсветки.

Подсветка совпадений без XSS

Частый кейс — подсветка введённого текста:

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

return item.text.replace(query, `<b>${query}</b>`);

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

const safeText = escapeHTML(item.text);
const safeQuery = escapeHTML(query);

return safeText.replace(
  new RegExp(safeQuery, "gi"),
  (match) => `<b>${match}</b>`
);

Либо более безопасный DOM-вариант:

const span = document.createElement("span");
span.textContent = item.text;

Модель угроз в компонентах выбора

Для Tom Select ключевые источники XSS можно классифицировать:

  • внешние API (AJAX);
  • пользовательский ввод (create);
  • кастомные рендеры;
  • сохранённые значения в базе данных;
  • подсветка и форматирование;
  • сторонние плагины.

Каждый из этих каналов должен считаться недоверенным до момента экранирования.


Принципы безопасной архитектуры рендера

Системный подход к защите включает:

  • запрет прямого innerHTML для пользовательских данных;
  • обязательное использование textContent или экранирования;
  • централизованную функцию sanitization;
  • единый слой формирования UI-данных;
  • отсутствие HTML в API-слое;
  • отказ от inline-событий и динамических атрибутов.

Такой подход минимизирует вероятность внедрения XSS даже при сложных кастомных интерфейсах селекторов.