Безопасная работа с шаблонами

В библиотеке Choices.js система шаблонов определяет, каким образом элементы списка, выбранные значения и подсказки превращаются в DOM-структуры. Основной риск возникает из-за того, что шаблоны часто получают данные из внешних источников: API, пользовательского ввода, локального хранилища. Любая точка входа данных становится потенциальным вектором внедрения HTML и JavaScript.

Ключевая проблема заключается в том, что шаблон в JavaScript нередко реализуется через строковую конкатенацию и присваивание через innerHTML. В таких условиях содержимое элемента перестает быть просто текстом и начинает интерпретироваться как разметка.


Поведение Choices.js при рендеринге элементов

Choices.js предоставляет возможность кастомизации отображения через функции:

  • itemTemplate
  • choiceTemplate
  • callbackOnCreateTemplates
  • renderChoiceLimit

Во многих реализациях шаблон представляет собой функцию, возвращающую строку HTML. Именно этот подход создаёт потенциальную поверхность для XSS, если данные не проходят обработку.

Типичный небезопасный паттерн:

const choices = new Choices('#select', {
  itemTemplate: (item) => `
    <div class="custom-item">
      ${item.label}
    </div>
  `
});

Если item.label содержит <script> или event-атрибуты вроде onerror, они будут интерпретированы браузером.


Источники внедрения вредоносной разметки

Основные источники опасных данных:

  • ответы серверного API без экранирования
  • значения из URL-параметров
  • данные из localStorage/sessionStorage
  • пользовательский ввод в режиме addItem
  • импортированные JSON-списки

Особую опасность представляют сценарии, где Choices.js используется как UI-обёртка для динамических тегов или autocomplete, так как пользователь напрямую влияет на создаваемые элементы.


Разделение текстового и HTML-контекста

Безопасная архитектура шаблонов опирается на принцип: данные всегда остаются данными, пока явно не превращены в DOM-узлы.

Корректный подход заключается в отказе от строкового HTML в пользу DOM API:

itemTemplate: (item) => {
  const div = document.createElement('div');
  div.className = 'custom-item';
  div.textContent = item.label;
  return div;
}

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


Опасность innerHTML и шаблонных строк

Использование innerHTML или шаблонных строк с вставкой переменных без экранирования приводит к прямой возможности внедрения скриптов.

Пример уязвимого подхода:

const html = `<span>${userInput}</span>`;
element.innerHTML = html;

Даже минимальное загрязнение входных данных приводит к выполнению произвольного кода.


Экранирование как обязательный слой защиты

При необходимости генерации HTML-строк требуется ручное экранирование:

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

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

itemTemplate: (item) => `
  <div class="custom-item">
    ${escapeHTML(item.label)}
  </div>
`

Этот подход снижает риск, но не устраняет его полностью при сложных сценариях композиции HTML.


Безопасные шаблоны через DOM-узлы

Наиболее устойчивый вариант — отказ от 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;
}

Такой подход исключает интерпретацию любых входных данных как кода.


Обработка пользовательского ввода в режиме addItem

Режим добавления новых элементов является одним из самых чувствительных участков. Пользовательский ввод напрямую становится частью внутренней модели данных.

Уязвимый сценарий:

choices.setValue([{ value: userInput, label: userInput }]);

Если далее этот label используется в HTML-шаблонах без экранирования, возникает цепочка XSS.

Безопасная модель:

  • хранение сырого значения отдельно от отображаемого
  • обязательное экранирование при рендере
  • валидация допустимых символов

Интеграция с серверными данными

При загрузке данных через API предполагается, что источник может быть частично или полностью недоверенным.

Безопасный пайплайн обработки:

  1. получение JSON
  2. нормализация структуры
  3. валидация типов
  4. экранирование перед отображением

Пример нормализации:

function normalizeChoice(data) {
  return {
    value: String(data.value),
    label: String(data.label)
  };
}

CSP как дополнительный уровень защиты

Content Security Policy снижает последствия XSS, ограничивая выполнение inline-скриптов и загрузку внешних ресурсов.

Базовая политика:

Content-Security-Policy: default-src 'self'; script-src 'self';

Даже при наличии уязвимости внедрённый <script> не будет выполнен, если политика строго запрещает inline execution.


Санитизация HTML при необходимости его сохранения

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

Принцип работы санитайзера:

  • удаление <script>
  • удаление inline-обработчиков событий
  • разрешение только безопасных тегов (b, i, span)

Пример концептуального использования:

itemTemplate: (item) => {
  const safeHTML = sanitize(item.label);
  const container = document.createElement('div');
  container.innerHTML = safeHTML;
  return container;
}

Даже в этом случае предпочтение остаётся за DOM-методами, а не HTML-строками.


Типовые ошибки при кастомизации Choices.js

Наиболее распространённые ошибки:

  • возврат строк HTML вместо DOM-узлов
  • отсутствие экранирования в шаблонах
  • доверие данным API без валидации
  • смешивание логики и представления в шаблонах
  • использование innerHTML в callback-функциях библиотеки

Каждая из этих ошибок усиливает поверхность атаки при работе с динамическими списками.


Принципы безопасного построения шаблонов

Архитектура шаблонов должна следовать нескольким устойчивым правилам:

  • данные не интерпретируются как HTML по умолчанию
  • DOM создаётся через API браузера, а не через строки
  • любой внешний ввод считается недоверенным
  • отображение и данные разделены на уровне структуры
  • экранирование применяется даже при частичной уверенности в источнике

Такой подход делает работу Choices.js предсказуемой даже при высокой динамичности данных и сложной интеграции с внешними источниками.