В библиотеке Choices.js система шаблонов определяет, каким образом элементы списка, выбранные значения и подсказки превращаются в DOM-структуры. Основной риск возникает из-за того, что шаблоны часто получают данные из внешних источников: API, пользовательского ввода, локального хранилища. Любая точка входа данных становится потенциальным вектором внедрения HTML и JavaScript.
Ключевая проблема заключается в том, что шаблон в JavaScript нередко
реализуется через строковую конкатенацию и присваивание через
innerHTML. В таких условиях содержимое элемента перестает
быть просто текстом и начинает интерпретироваться как разметка.
Choices.js предоставляет возможность кастомизации отображения через функции:
itemTemplatechoiceTemplatecallbackOnCreateTemplatesrenderChoiceLimitВо многих реализациях шаблон представляет собой функцию, возвращающую строку HTML. Именно этот подход создаёт потенциальную поверхность для XSS, если данные не проходят обработку.
Типичный небезопасный паттерн:
const choices = new Choices('#select', {
itemTemplate: (item) => `
<div class="custom-item">
${item.label}
</div>
`
});
Если item.label содержит <script> или
event-атрибуты вроде onerror, они будут интерпретированы
браузером.
Основные источники опасных данных:
addItemОсобую опасность представляют сценарии, где Choices.js используется как UI-обёртка для динамических тегов или autocomplete, так как пользователь напрямую влияет на создаваемые элементы.
Безопасная архитектура шаблонов опирается на принцип: данные всегда остаются данными, пока явно не превращены в DOM-узлы.
Корректный подход заключается в отказе от строкового HTML в пользу DOM API:
itemTemplate: (item) => {
const div = document.createElement('div');
div.className = 'custom-item';
div.textContent = item.label;
return div;
}
Использование textContent гарантирует, что любые символы
<, >, & не будут
интерпретированы как HTML.
Использование innerHTML или шаблонных строк с вставкой
переменных без экранирования приводит к прямой возможности внедрения
скриптов.
Пример уязвимого подхода:
const html = `<span>${userInput}</span>`;
element.innerHTML = html;
Даже минимальное загрязнение входных данных приводит к выполнению произвольного кода.
При необходимости генерации HTML-строк требуется ручное экранирование:
function escapeHTML(str) {
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
Использование:
itemTemplate: (item) => `
<div class="custom-item">
${escapeHTML(item.label)}
</div>
`
Этот подход снижает риск, но не устраняет его полностью при сложных сценариях композиции HTML.
Наиболее устойчивый вариант — отказ от HTML-строк полностью:
choiceTemplate: (choice) => {
const wrapper = document.createElement('div');
wrapper.className = 'choice';
const label = document.createElement('span');
label.textContent = choice.label;
wrapper.appendChild(label);
return wrapper;
}
Такой подход исключает интерпретацию любых входных данных как кода.
Режим добавления новых элементов является одним из самых чувствительных участков. Пользовательский ввод напрямую становится частью внутренней модели данных.
Уязвимый сценарий:
choices.setValue([{ value: userInput, label: userInput }]);
Если далее этот label используется в HTML-шаблонах без
экранирования, возникает цепочка XSS.
Безопасная модель:
При загрузке данных через API предполагается, что источник может быть частично или полностью недоверенным.
Безопасный пайплайн обработки:
Пример нормализации:
function normalizeChoice(data) {
return {
value: String(data.value),
label: String(data.label)
};
}
Content Security Policy снижает последствия XSS, ограничивая выполнение inline-скриптов и загрузку внешних ресурсов.
Базовая политика:
Content-Security-Policy: default-src 'self'; script-src 'self';
Даже при наличии уязвимости внедрённый <script> не
будет выполнен, если политика строго запрещает inline execution.
В случаях, когда HTML-контент допустим (например, подсветка текста или форматирование), применяется санитизация через специализированные библиотеки или строгие фильтры.
Принцип работы санитайзера:
<script>b, i,
span)Пример концептуального использования:
itemTemplate: (item) => {
const safeHTML = sanitize(item.label);
const container = document.createElement('div');
container.innerHTML = safeHTML;
return container;
}
Даже в этом случае предпочтение остаётся за DOM-методами, а не HTML-строками.
Наиболее распространённые ошибки:
innerHTML в callback-функциях
библиотекиКаждая из этих ошибок усиливает поверхность атаки при работе с динамическими списками.
Архитектура шаблонов должна следовать нескольким устойчивым правилам:
Такой подход делает работу Choices.js предсказуемой даже при высокой динамичности данных и сложной интеграции с внешними источниками.