Использование функций рендеринга для безопасности

Функции рендеринга в Tom Select определяют, каким образом данные элементов превращаются в DOM-структуру внутри выпадающего списка, выбранных значений и вспомогательных компонентов интерфейса. Именно через эти функции чаще всего формируется поверхность атак XSS, если данные приходят из внешних источников или недостаточно очищены.

Архитектура рендеринга основана на наборе callback-функций, возвращающих HTML-строки или DOM-элементы. Это создаёт гибкость, но одновременно переносит ответственность за безопасность на разработчика.


Поверхность атаки: от данных к HTML

Каждый объект опции в Tom Select может содержать произвольные поля:

  • text
  • value
  • дополнительные метаданные (email, avatar, html, role и т.д.)

Эти данные используются в render-функциях:

  • render.option
  • render.item
  • render.option_create
  • render.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 при рендеринге выпадающего списка.


Безопасный подход: отказ от HTML-конкатенации

Наиболее устойчивый вариант — использование 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("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

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

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 не экранируется
  • HTML строится строкой

Даже если 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;

Рендеринг выбранных элементов (item)

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)

Функция 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 важно разделять источники данных:

  • серверные данные — потенциально изменяемы через API-атаки
  • пользовательский ввод — полностью недоверенный
  • локальные кэши — могут быть скомпрометированы XSS ранее

Любая render-функция должна рассматривать входные данные как недоверенные по умолчанию.


Опасные паттерны, усиливающие XSS

Некоторые конструкции многократно увеличивают риск:

  • использование innerHTML внутри render
  • вставка необработанных JSON-полей
  • генерация HTML через шаблоны строк
  • отключение экранирования ради “удобства верстки”
  • использование сторонних HTML-генераторов без фильтрации

Рекомендованный безопасный шаблон рендеринга

Наиболее устойчивый подход объединяет три принципа:

  • отказ от строкового HTML
  • использование textContent
  • явная проверка URL и типов
new 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 также возможны:

  • подмена отображаемого текста через ::before
  • внедрение URL в background-image
  • эксплуатация style через динамические поля

Поэтому любые данные в style-атрибутах должны проходить строгую фильтрацию или полностью исключаться.


Итоговые принципы безопасного рендеринга

Внутри Tom Select безопасность рендеринга определяется не библиотекой, а дисциплиной формирования DOM:

  • HTML-строки считаются небезопасными по умолчанию
  • textContent предпочтительнее любых шаблонов
  • DOM-методы исключают большинство классов XSS
  • данные из API рассматриваются как недоверенные
  • render-функции — зона повышенного риска, а не удобства