XSS-уязвимости при работе с DOM

XSS (Cross-Site Scripting) — класс уязвимостей, позволяющий внедрять и исполнять произвольный JavaScript-код в браузере пользователя. В случае работы с DOM речь чаще всего идёт о DOM-based XSS, при которой вредоносный код не приходит с сервера, а формируется и исполняется непосредственно в клиентском JavaScript.

В отличие от отражённых (reflected) и хранимых (stored) XSS, здесь источник данных — это:

  • window.location (URL, hash, query-параметры)
  • document.cookie
  • localStorage / sessionStorage
  • данные из postMessage
  • сторонние API или пользовательский ввод

Основная проблема — небезопасная вставка этих данных в DOM без фильтрации или экранирования.


Типичные источники данных (sources) и точки вставки (sinks)

Источники (sources)

Данные, потенциально контролируемые злоумышленником:

  • location.search
  • location.hash
  • document.referrer
  • window.name
  • localStorage.getItem()

Приёмники (sinks)

Места, где данные могут привести к выполнению кода:

  • innerHTML
  • outerHTML
  • insertAdjacentHTML
  • document.write
  • eval
  • setTimeout / setInterval (со строкой)
  • new Function()

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

const hash = window.location.hash.substring(1);
document.getElementById('output').innerHTML = hash;

Если URL содержит:

#<img src=x oner ror=alert(1)>

произойдёт выполнение alert(1).


Особенности XSS при использовании Headroom.js

Библиотека Headroom.js управляет поведением заголовков (header) при прокрутке страницы. Она не занимается обработкой пользовательских данных напрямую, но часто используется в проектах, где:

  • активно манипулируется DOM
  • применяются динамические классы и состояния
  • используется взаимодействие с пользовательским вводом

Это создаёт условия, при которых XSS может быть внедрён через сопутствующий код.

Пример интеграции

const header = document.querySelector("header");
const headroom = new Headroom(header);
headroom.init();

Проблема возникает не в самой библиотеке, а в окружении:

const title = new URLSearchParams(location.search).get('title');
header.innerHTML = `<h1>${title}</h1>`;

Если title содержит вредоносный код, он будет вставлен в DOM и выполнен.


Опасные шаблоны при работе с DOM

1. Использование innerHTML

element.innerHTML = userInput;

Опасность: интерпретация HTML и выполнение встроенных обработчиков (onerror, onclick).

2. Шаблонные строки без экранирования

element.innerHTML = `<div>${data}</div>`;

Даже при использовании современных возможностей JavaScript проблема остаётся.

3. Динамическое создание атрибутов

element.setAttribute("onclick", userInput);

Позволяет внедрить произвольный JavaScript.

4. Использование eval

eval(userInput);

Прямое выполнение кода — критическая уязвимость.


Безопасные альтернативы

textContent вместо innerHTML

element.textContent = userInput;

Гарантирует, что данные будут интерпретированы как текст.

createElement + appendChild

const div = document.createElement("div");
div.textContent = userInput;
element.appendChild(div);

Исключает интерпретацию HTML.

setAttribute с проверкой

if (isSafeUrl(url)) {
    element.setAttribute("href", url);
}

Контекстно-зависимое экранирование

Разные контексты требуют разных подходов:

Контекст Метод защиты
HTML HTML-экранирование
Атрибут Экранирование + whitelist
JavaScript JSON.stringify
URL encodeURIComponent

Пример:

const safe = encodeURIComponent(userInput);
link.href = `/search?q=${safe}`;

Использование Content Security Policy (CSP)

CSP — механизм браузера, ограничивающий выполнение скриптов.

Пример заголовка:

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

Преимущества:

  • блокирует inline-скрипты
  • предотвращает выполнение внедрённого кода
  • снижает риск эксплуатации XSS

Санитизация HTML

Когда необходимо вставлять HTML (например, пользовательский контент), применяется очистка:

DOMPurify

const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;

Удаляет:

  • <script>
  • опасные атрибуты (onerror, onclick)
  • вредоносные URL (jav * ascript:)

Уязвимости через события и обработчики

element.oncl ick = new Function(userInput);

или:

setTimeout(userInput, 1000);

Такие конструкции позволяют выполнить любой код.

Безопасный вариант:

setTimeout(() => {
    console.log("safe");
}, 1000);

Особенности работы с URL и hash

Headroom.js часто используется на страницах с прокруткой и якорями (#section).

Уязвимость:

const section = location.hash.slice(1);
document.getElementById(section).scrollIntoView();

Если section используется далее в HTML:

element.innerHTML = section;

возникает риск XSS.


Защита при работе с localStorage

const data = localStorage.getItem("content");
element.innerHTML = data;

Если злоумышленник смог записать данные в localStorage, они будут выполнены.

Решение:

  • очищать данные перед сохранением
  • валидировать перед использованием

Проверка и валидация данных

Основные подходы:

  • whitelist (разрешённые значения)
  • строгая типизация
  • регулярные выражения

Пример:

function isSafeText(str) {
    return /^[a-zA-Z0-9\s]+$/.test(str);
}

Инструменты для анализа

  • ESLint с правилами безопасности
  • Snyk
  • OWASP ZAP
  • Burp Suite

Автоматический поиск:

  • использования innerHTML
  • вызовов eval
  • небезопасных атрибутов

Практика безопасной интеграции Headroom.js

  1. Изоляция логики

    • Headroom должен управлять только классами и состоянием
  2. Отказ от динамической HTML-вставки

    • использовать только безопасные методы
  3. Контроль данных из URL

    • не использовать напрямую
  4. Минимизация inline-скриптов

    • перенос логики в JS-файлы
  5. Использование CSP

    • запрет inline и eval

Типичный безопасный шаблон

const params = new URLSearchParams(location.search);
const title = params.get("title");

const header = document.querySelector("header");

const h1 = document.createElement("h1");
h1.textContent = title || "Default";

header.appendChild(h1);

const headroom = new Headroom(header);
headroom.init();

Частые ошибки разработчиков

  • доверие данным из URL
  • смешивание данных и HTML
  • использование innerHTML «для удобства»
  • игнорирование CSP
  • недостаточная валидация

Влияние XSS на безопасность приложения

Последствия:

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

Особенно критично при:

  • авторизованных сессиях
  • админ-панелях
  • работе с токенами

Связь с архитектурой фронтенда

Современные фреймворки (React, Vue) минимизируют риск XSS за счёт:

  • автоматического экранирования
  • виртуального DOM
  • ограничений на прямую работу с HTML

Однако при использовании:

  • dangerouslySetInnerHTML
  • сторонних библиотек
  • ручной работы с DOM

риски возвращаются.


Выводы для практики

  • XSS в DOM возникает из-за неправильной работы с данными
  • Headroom.js не является источником уязвимости, но может использоваться в небезопасной среде
  • основной принцип — никогда не вставлять непроверенные данные как HTML
  • безопасные API браузера должны использоваться по умолчанию
  • защита должна быть многоуровневой: код + политика браузера + инструменты анализа