Функции рендеринга в Tom Select определяют, каким образом данные элементов превращаются в DOM-структуру внутри выпадающего списка, выбранных значений и вспомогательных компонентов интерфейса. Именно через эти функции чаще всего формируется поверхность атак XSS, если данные приходят из внешних источников или недостаточно очищены.
Архитектура рендеринга основана на наборе callback-функций, возвращающих HTML-строки или DOM-элементы. Это создаёт гибкость, но одновременно переносит ответственность за безопасность на разработчика.
Каждый объект опции в Tom Select может содержать произвольные поля:
textvalueЭти данные используются в render-функциях:
render.optionrender.itemrender.option_createrender.optgroup_headerЕсли хотя бы одно поле попадает в HTML без экранирования, возникает
риск внедрения скриптов через innerHTML.
Наиболее распространённая ошибка — прямое вставление строк в HTML:
new TomSelect("#select", {
render: {
option(data) {
return `<div class="option">
<span class="label">${data.text}</span>
</div>`;
}
}
});
Если data.text приходит с сервера или из
пользовательского ввода, возможен сценарий:
{
"text": "<img src=x oner ror=alert(1)>"
}
Результат — выполнение произвольного JavaScript при рендеринге выпадающего списка.
Наиболее устойчивый вариант — использование DOM API вместо строк:
new TomSelect("#select", {
render: {
option(data) {
const div = document.createElement("div");
div.className = "option";
const span = document.createElement("span");
span.className = "label";
span.textContent = data.text;
div.appendChild(span);
return div;
}
}
});
Ключевой момент: textContent полностью исключает
интерпретацию HTML.
Если DOM API недоступен, применяется ручное экранирование:
function escapeHtml(str) {
return String(str)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
Использование:
render: {
item(data) {
return `<div class="item">
${escapeHtml(data.text)}
</div>`;
}
}
Этот подход снижает риск, но остаётся чувствительным к ошибкам разработчика.
В Tom Select рендер-функции часто становятся местом агрегации данных:
render: {
option(data) {
return `<div>
<img src="${data.avatar}">
<span>${data.name}</span>
</div>`;
}
}
Здесь возникает сразу несколько проблем:
src не валидируетсяname не экранируетсяДаже если name безопасен сегодня, изменение API сервера
делает код уязвимым.
Особую опасность представляют HTML-атрибуты (href,
src, style, data-*).
Небезопасный пример:
return `<img src="${data.avatarUrl}">`;
Если avatarUrl содержит:
jav * ascript:alert(1)
или
" oner ror="alert(1)
возможна эксплуатация.
Безопасный вариант:
const img = document.createElement("img");
if (typeof data.avatarUrl === "string" && data.avatarUrl.startsWith("https://")) {
img.src = data.avatarUrl;
}
img.alt = data.text || "";
return img;
render.item используется для отображения выбранных
значений внутри инпута.
Это критическая точка, поскольку отображение происходит постоянно при изменениях состояния:
render: {
item(data) {
const el = document.createElement("div");
el.className = "selected-item";
const label = document.createElement("span");
label.textContent = data.text;
el.appendChild(label);
return el;
}
}
Использование innerHTML в этом контексте особенно
опасно, так как выбранные элементы могут формироваться через API, импорт
данных или localStorage.
Функция option_create часто игнорируется с точки зрения
безопасности, хотя именно туда пользовательский ввод попадает
напрямую.
render: {
option_create(data) {
return `<div class="create">
Создать: ${data.input}
</div>`;
}
}
Здесь data.input — чистый пользовательский ввод.
Безопасный вариант:
render: {
option_create(data) {
const div = document.createElement("div");
div.className = "create";
const text = document.createElement("span");
text.textContent = `Создать: ${data.input}`;
div.appendChild(text);
return div;
}
}
В Tom Select важно разделять источники данных:
Любая render-функция должна рассматривать входные данные как недоверенные по умолчанию.
Некоторые конструкции многократно увеличивают риск:
innerHTML внутри renderНаиболее устойчивый подход объединяет три принципа:
textContentnew TomSelect("#select", {
render: {
option(data) {
const root = document.createElement("div");
root.className = "option";
const title = document.createElement("div");
title.textContent = data.text;
const meta = document.createElement("small");
meta.textContent = data.meta || "";
root.appendChild(title);
root.appendChild(meta);
return root;
}
}
});
Хотя основной риск связан с XSS, визуальные атаки через CSS также возможны:
::beforebackground-imagestyle через динамические поляПоэтому любые данные в style-атрибутах должны проходить строгую фильтрацию или полностью исключаться.
Внутри Tom Select безопасность рендеринга определяется не библиотекой, а дисциплиной формирования DOM:
textContent предпочтительнее любых шаблонов