XSS-уязвимости при вставке контента

XSS (Cross-Site Scripting) — это тип уязвимости веб-приложений, при которой злоумышленник может внедрять вредоносный скрипт в страницы, просматриваемые другими пользователями. Эти скрипты исполняются в браузере жертвы с правами пользователя, что позволяет похищать данные, изменять содержимое страницы или выполнять нежелательные действия от имени пользователя.

Существуют три основных типа XSS:

  1. Reflected XSS – скрипт внедряется в URL или форму и исполняется сразу при обработке запроса.
  2. Stored XSS – вредоносный код сохраняется на сервере, например в базе данных, и выполняется при загрузке страницы.
  3. DOM-based XSS – скрипт внедряется через манипуляции с DOM на стороне клиента без участия сервера.

Механизм возникновения XSS при динамическом контенте

Любой сценарий, где данные извлекаются из внешнего источника и вставляются на страницу без фильтрации, потенциально опасен. Примеры источников:

  • Поля формы пользователя
  • Параметры URL
  • Ответы API
  • Данные из базы данных

Ключевой фактор — отсутствие экранирования специальных символов HTML и JavaScript. Символы <, >, ", ', & могут позволить внедрить теги <script> или обработчики событий, такие как onclick, что приведет к выполнению произвольного кода.

Уязвимости при работе с Intro.js

Intro.js позволяет создавать интерактивные туры по интерфейсу веб-приложения, вставляя подсказки (tooltips) и выделяя элементы страницы. Для вставки текста используется свойство intro:

introJs().setOptions({
    steps: [
        {
            element: '#step1',
            intro: 'Добро пожаловать на сайт!'
        }
    ]
}).start();

Если значение intro формируется из пользовательского ввода, любая HTML-теговая или скриптовая вставка может стать уязвимостью XSS:

let userInput = getUserInput(); // допустим, пользователь ввел <img src=x oner ror=alert(1)>
introJs().setOptions({
    steps: [
        {
            element: '#step1',
            intro: userInput
        }
    ]
}).start();

В этом случае скрипт <img src=x oner ror=alert(1)> выполнится в браузере любого посетителя.

Способы предотвращения XSS в контексте Intro.js

  1. Экранирование HTML Все пользовательские данные должны быть преобразованы, чтобы символы <, >, &, " и ' отображались как текст, а не как код:
function escapeHtml(text) {
    return text
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

let safeInput = escapeHtml(getUserInput());
introJs().setOptions({
    steps: [
        { element: '#step1', intro: safeInput }
    ]
}).start();
  1. Использование textContent вместо innerHTML Внутри шагов Intro.js библиотека по умолчанию вставляет HTML. Для кастомных контейнеров, если есть возможность, следует использовать textContent вместо прямого внедрения HTML.

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

  3. Применение Content Security Policy (CSP) CSP позволяет запрещать выполнение встроенного JavaScript, что ограничивает последствия возможного XSS:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
  1. Шаблонизация через безопасные библиотеки Вместо динамического конкатенирования HTML для intro использовать безопасные шаблонизаторы или функции, которые автоматически экранируют данные.

Примеры безопасного использования Intro.js

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

const userNotes = fetchUserNotes(); // массив заметок
const steps = userNotes.map(note => ({
    element: '#note-' + note.id,
    intro: escapeHtml(note.text)
}));

introJs().setOptions({ steps }).start();

В этом примере любая попытка вставить HTML или скрипт пользователем нейтрализуется функцией escapeHtml.

Добавление элементов с заранее определенным HTML, исключая пользовательский ввод:

introJs().setOptions({
    steps: [
        {
            element: '#feature1',
            intro: 'Используйте эту кнопку для сохранения данных.'
        }
    ]
}).start();

Здесь все строки заданы разработчиком, что полностью исключает риск XSS.

Риски при хранении шагов на сервере

Если структура шагов сохраняется в базе данных для последующего отображения, необходимо:

  • Экранировать или валидировать данные перед сохранением
  • Обеспечить повторное экранирование при выводе на страницу
  • Исключить любые теги <script> и обработчики событий

Особенности DOM-based XSS с Intro.js

Intro.js взаимодействует с DOM напрямую, выделяя элементы и вставляя подсказки. Если пользователь может контролировать селекторы элементов или содержимое шага, возможен DOM-based XSS. Примеры опасных сценариев:

  • Вставка произвольного селектора через URL-параметр
  • Использование данных из localStorage или sessionStorage без фильтрации

Защита: никогда не использовать пользовательский ввод напрямую для селекторов или HTML-контента.

Итоговые рекомендации по безопасной интеграции Intro.js

  • Всегда экранировать пользовательский ввод для intro.
  • Предпочитать текстовые подсказки без HTML, если нет строгой необходимости.
  • Валидация и фильтрация данных на сервере и клиенте.
  • Использовать CSP для снижения последствий возможного XSS.
  • Избегать прямой передачи пользовательских данных в селекторы и другие элементы DOM, управляемые Intro.js.

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