Экранирование HTML

Обработка пользовательских данных внутри компонентов выбора напрямую связана с безопасностью интерфейса, поскольку любое значение, попадающее в DOM без фильтрации, потенциально может стать вектором XSS-атаки. В контексте кастомных селектов это особенно критично: список опций часто формируется из внешних источников — API, пользовательского ввода, CMS или базы данных.

Проблема внедрения HTML через значения опций

Типичная модель работы Slim Select предполагает преобразование массива данных в интерактивный список. При этом значения text и value нередко используются напрямую при рендеринге:

new SlimSelect({
  select: '#example',
  data: [
    { text: 'Admin', value: 'admin' },
    { text: 'User', value: 'user' }
  ]
});

При безопасных данных такой подход не вызывает проблем. Однако при наличии неэкранированного контента:

{ text: '<img src=x oner ror=alert(1)>', value: 'xss' }

результат зависит от того, каким образом библиотека вставляет данные в DOM: через textContent или через innerHTML. В случае использования HTML-рендеринга риск выполнения скрипта становится прямым.

Механика экранирования и точки внедрения

В интерфейсах селектов существует несколько слоёв, где происходит вставка данных:

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

Каждая из этих точек может использовать разные методы вставки DOM-данных. Ключевое различие:

  • textContent — безопасное отображение текста
  • innerHTML — интерпретация строки как HTML

Если библиотека или кастомный renderer использует второй вариант без экранирования, возникает потенциальная уязвимость.

Поведение Slim Select при стандартном рендеринге

В базовой конфигурации Slim Select ориентируется на безопасное отображение текстовых значений. Это означает, что при стандартном использовании:

  • текст опции вставляется как текстовый узел
  • HTML-теги не интерпретируются
  • специальные символы отображаются буквально

Пример безопасного поведения:

{ text: '<b>Admin</b>', value: 'admin' }

Отображается как:

<b>Admin</b>

без форматирования.

Опасные сценарии: кастомный render

Риски возникают при использовании кастомных функций рендеринга, где разработчик получает полный контроль над HTML:

new SlimSelect({
  select: '#example',
  data: [
    { text: 'Admin', value: 'admin' }
  ],
  render: {
    option: (data) => `<div class="item">${data.text}</div>`
  }
});

В данном случае строка вставляется напрямую в HTML-шаблон. Если data.text содержит вредоносный код, он будет исполнен браузером.

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

{
  text: '<svg onl oad=alert(document.cookie)>',
  value: 'x'
}

Принципы экранирования

Экранирование HTML заключается в замене специальных символов на безопасные HTML-сущности:

Символ Замена
< &lt;
> &gt;
& &amp;
" &quot;
' &#039;

Функция экранирования может быть реализована вручную:

function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

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

render: {
  option: (data) => `<div>${escapeHtml(data.text)}</div>`
}

Разделение ответственности: данные и представление

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

Подходы:

  • хранение чистых строк в источнике данных
  • экранирование на границе UI-слоя
  • отказ от генерации HTML внутри бизнес-логики

Нарушение этого разделения приводит к непредсказуемым эффектам, особенно при динамическом обновлении списка.

Фильтрация входных данных перед передачей в Slim Select

Перед инициализацией компонента данные могут проходить нормализацию:

function sanitizeData(items) {
  return items.map(item => ({
    text: escapeHtml(item.text),
    value: item.value
  }));
}

Однако важно различать экранирование и санитизацию:

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

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

Использование textContent как базовый механизм защиты

DOM API предоставляет встроенный механизм безопасной вставки:

const el = document.createElement('div');
el.textContent = userInput;

Это эквивалентно автоматическому экранированию.

В контексте кастомных компонентов Slim Select предпочтительно использовать именно этот подход, если нет необходимости в HTML:

render: {
  option: (data) => {
    const div = document.createElement('div');
    div.textContent = data.text;
    return div;
  }
}

Поиск и фильтрация как дополнительные точки риска

Даже если рендеринг защищён, уязвимость может проявляться через:

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

Если фильтрация реализована на основе innerHTML, то вредоносные строки могут попасть в DOM до экранирования.

Корректный подход:

searchFilter: (option, search) => {
  return option.text.toLowerCase().includes(search.toLowerCase());
}

Фильтрация должна работать только с текстовыми значениями, без HTML-интерпретации.

Комбинированные атаки через атрибуты и события

Даже при экранированном текстовом содержимом возможны атаки через атрибуты:

<div oncl ick="alert(1)">item</div>

Если render-функция формирует HTML-строку, любое динамическое значение в атрибутах должно проходить экранирование или полное исключение.

Безопасная практика — избегать inline-обработчиков событий:

  • запрет on* атрибутов
  • использование addEventListener
  • отказ от генерации событий в строках

Политика безопасного рендера в UI-компонентах

Для компонентов выбора характерны следующие принципы:

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

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

Практическая модель безопасного шаблонизатора

Универсальный подход к безопасной генерации HTML:

function createOption(data) {
  return `
    <div class="option" data-value="${escapeHtml(data.value)}">
      ${escapeHtml(data.text)}
    </div>
  `;
}

Важно, что экранирование применяется:

  • к текстовому содержимому
  • к атрибутам
  • к динамическим значениям

Итоговая модель обработки данных

Безопасная цепочка обработки данных в селекте:

  1. получение исходных данных
  2. валидация структуры
  3. экранирование при рендере
  4. вставка через безопасные DOM-методы
  5. исключение HTML-интерпретации

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