Компоненты выбора с динамическими списками, такие как Tom Select,
часто работают с пользовательскими или удалёнными данными, что создаёт
прямую поверхность атаки для XSS (Cross-Site Scripting). Основная
проблема возникает в момент, когда текст из источника данных без
обработки попадает в DOM через innerHTML или аналогичные
механизмы рендеринга.
Особенно уязвимыми становятся сценарии:
create: true);render.option,
render.item);Любая строка, содержащая HTML-разметку, потенциально превращается в исполняемый скрипт, если библиотека или пользовательский код вставляет её без экранирования.
В Tom Select визуализация опций основана на функциях рендера:
render.optionrender.itemrender.option_createКаждая из них возвращает HTML-строку или DOM-структуру, которая затем вставляется в список.
Уязвимость появляется, если используется прямое внедрение данных:
render: {
option: function(data) {
return `<div>${data.text}</div>`;
}
}
Если data.text содержит
<script>alert(1)</script>, он выполнится при
вставке в DOM.
Основной принцип защиты — исключение интерпретации HTML через экранирование специальных символов.
Минимальный набор символов:
< → <> → >& → &" → "' → 'В контексте Tom Select безопасный рендеринг должен использовать функцию экранирования:
function escapeHTML(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Использование в рендере:
render: {
option: function(data) {
return `<div>${escapeHTML(data.text)}</div>`;
},
item: function(data) {
return `<div>${escapeHTML(data.text)}</div>`;
}
}
Ключевой принцип: любой внешний ввод трактуется как текст, а не HTML.
В Tom Select встречается сценарий, когда данные приходят с сервера уже в виде HTML:
{
"text": "<b>Admin</b>"
}
Если включена логика отображения без экранирования, такой текст становится исполняемой разметкой.
Политика безопасности должна быть однозначной:
При кастомном рендеринге важно избегать прямой конкатенации строк.
Небезопасный вариант:
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, так как браузер интерпретирует содержимое как текстовый узел.
Функция создания новых значений часто становится точкой 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:
Рекомендуемая модель обработки:
Пример:
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
})));
});
}
Наиболее надёжный подход в 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;
}
}
Преимущества:
В сложных случаях, когда требуется разрешённая HTML-разметка (например, жирный текст или подсветка совпадений), применяется санитизация через специализированные библиотеки.
Типичный подход:
onclick,
onerror);Пример с DOMPurify-подобной логикой:
const clean = sanitizeHTML(dirtyHTML);
render: {
option: (data) => `<div>${clean(data.html)}</div>`
}
Важно: санитизация не заменяет экранирование, а является альтернативой только при необходимости HTML-рендеринга.
Функция поиска внутри списка также может использовать пользовательский ввод:
new TomSelect("#select", {
searchField: ["text"]
});
Хотя поиск не вставляет HTML напрямую, он может:
render.Поэтому результат поиска должен проходить те же правила:
innerHTML;Частый кейс — подсветка введённого текста:
Небезопасный вариант:
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 можно классифицировать:
create);Каждый из этих каналов должен считаться недоверенным до момента экранирования.
Системный подход к защите включает:
innerHTML для пользовательских
данных;textContent или
экранирования;Такой подход минимизирует вероятность внедрения XSS даже при сложных кастомных интерфейсах селекторов.