HTML в интерфейсах выбора данных становится источником уязвимостей
тогда, когда библиотека или разработчик начинают подставлять
пользовательский ввод напрямую в innerHTML без
экранирования. В контексте Tom Select это особенно критично, поскольку
элементы списка, опции и создаваемые значения часто приходят из внешних
источников: API, форм пользователя, импортированных наборов данных.
Tom Select по умолчанию включает защитный механизм экранирования
HTML. Это означает, что строки, содержащие потенциально опасные
конструкции вроде <script> или
<img oner ror=...>, не интерпретируются как HTML, а
отображаются как обычный текст.
Ключевое поведение задаётся параметром:
escape_html: true
Это значение включено по умолчанию и отвечает за преобразование специальных символов:
< → <> → >& → &" → "' → 'Таким образом, даже если данные приходят из ненадёжного источника, они не превращаются в DOM-структуру.
Внутри Tom Select используется функция:
TomSelect.escape_html()
Она применяется ко всем строковым значениям, которые попадают в визуальные элементы списка: option, item, create-option и другие шаблоны отображения.
Пример поведения:
TomSelect.escape_html('<b>test</b>')
// <b>test</b>
Эта функция используется в стандартных рендерах, если разработчик не переопределяет шаблоны вручную.
Основной источник проблем с 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>`;
}
}
Структуры данных для Tom Select часто приходят в виде объектов:
{
value: "1",
text: "Example"
}
или групп:
{
optgroup: "Group <b>name</b>"
}
Если optgroup или text содержат HTML и не
экранируются при кастомном рендере, они могут быть вставлены в DOM без
обработки.
Особенно опасны сценарии:
load() из внешнего APIПри использовании динамической загрузки:
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.
Использование этого режима допустимо только при строгой гарантии, что:
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.
Ключевой принцип безопасной работы с Tom Select заключается в разделении:
Нельзя смешивать HTML-разметку и данные:
// опасный подход
text: "<b>Admin</b>"
Правильнее:
text: "Admin",
highlight: true
и уже в render решать, как отображать выделение.
Даже при использовании data-* атрибутов необходимо
учитывать, что они могут попасть в DOM:
render: {
option: function(data, escape) {
return `<div data-id="${escape(data.value)}">${escape(data.text)}</div>`;
}
}
Любая подстановка без экранирования может привести к разрыву атрибутов и внедрению HTML.
Безопасное использование экранирования строится на трёх уровнях:
escape_html: true)escape() в
render-функцияхНарушение любого уровня создаёт потенциальную точку внедрения HTML в DOM, особенно при кастомных шаблонах и загрузке внешних данных.