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

Одна строка из формы поиска может выполнить JavaScript в браузере другого пользователя, украсть данные из интерфейса или изменить отправляемый запрос. XSS возникает не потому, что разработчик «забыл экранирование» вообще, а потому, что вставил данные в неподходящий контекст: HTML, атрибут, URL, JavaScript или DOM-операцию. Ниже — практическая схема для веб-приложений, рассчитанная на Chromium 120+, Firefox 120+ и Safari 17+.
Что именно делает XSS опасным
XSS (Cross-Site Scripting) — выполнение внедрённого сценария в контексте доверенного сайта. Браузер считает такой код частью страницы: он может обращаться к DOM, отправлять запросы от имени пользователя, читать доступные скриптам данные из localStorage и изменять интерфейс.
HttpOnly-cookie JavaScript прочитать не может, но это не делает сессию полностью безопасной. Внедрённый код способен отправить запрос к вашему API из уже открытой сессии, если сервер не проверяет CSRF-защиту и права доступа. Поэтому XSS — не только проблема «всплывающего окна», а способ выполнять действия от имени пользователя.
Главное правило: защита выбирается по месту, куда попадают данные. Экранирование для HTML не становится безопасным экранированием для JavaScript или URL.
Контекст вывода — место, в котором браузер интерпретирует строку: текстовый узел, значение HTML-атрибута, адрес ссылки, CSS или программный код. Для каждого контекста нужны свои ограничения и способ обработки.
Три основных типа XSS
Хранимый XSS
Вредоносная строка сохраняется на сервере: в комментарии, профиле, названии товара, тикете поддержки или сообщении. Затем она попадает в ответ для каждого посетителя страницы. Такой вариант особенно опасен: атакующий внедряет данные один раз, а последствия получают все пользователи, которые открыли экран.
Проверяйте не только публичные страницы. Админ-панель часто становится более ценной целью: если сотрудник открывает заражённый отзыв, сценарий получает его права в интерфейсе управления.
Отражённый XSS
Ввод приходит в запросе и сразу возвращается в ответ. Типичный источник — параметр поиска, сообщение об ошибке или фильтр в URL. Атакующему приходится убедить жертву открыть специально сформированную ссылку, но это легко сделать через письмо, чат или поддельную страницу.
Современные браузеры блокируют часть подозрительных сценариев, но рассчитывать на это нельзя. Поведение зависит от контекста, заголовков и самого браузера, а корректная защита должна находиться в приложении.
DOM-based XSS
Сервер может вообще не участвовать в атаке: опасная строка попадает во фрагмент URL, значение формы или ответ API, а клиентский код вставляет её в DOM как HTML. Уязвимость ищут в цепочке «источник — обработка — опасный приём».
Частые источники — location.search, location.hash, данные из postMessage и ответы API. Опасные приёмы — innerHTML, outerHTML, insertAdjacentHTML, document.write и выполнение строки как JavaScript.
Почему одного экранирования недостаточно
Текст, который безопасен внутри обычного HTML-абзаца, может стать опасным внутри атрибута или обработчика события. Например, значение, выведенное в текстовый узел, не должно затем без проверки использоваться в href или передаваться в HTML-шаблон.
| Контекст | Безопасный базовый подход | Что ломает защиту |
|---|---|---|
| Текст страницы | Создавать текстовый узел или использовать шаблонизацию с автоэкранированием | Рендеринг строки как HTML |
| HTML-атрибут | Передавать значение через DOM-свойство или экранировать по правилам атрибута | Склейка строки HTML вручную |
| Ссылка | Разрешать только нужные схемы и домены, например https | Безусловный вывод пользовательского URL |
| HTML, который разрешён пользователю | Пропускать через проверенный sanitizer с ограниченным набором тегов | Самописная замена символов и «чистка» регулярными выражениями |
| JavaScript-код | Не вставлять пользовательские данные в код | Склейка строк с eval, обработчиками и inline-скриптами |
Практическая защита по слоям
1. Выводите данные как текст
Для обычного текста используйте API, который создаёт текстовое содержимое, а не разбирает строку как HTML. В браузерном JavaScript это textContent. В серверных шаблонах включайте автоматическое экранирование и не отключайте его без конкретной причины.
const message = new URLSearchParams(window.location.search).get('message') || '';
const output = document.querySelector('#message');
// Строка отображается как текст, а не как разметка.
output.textContent = message;
Если пользователю действительно нужны жирный текст, ссылки или списки, безопаснее разрешить небольшой набор элементов через поддерживаемый sanitizer и отдельную политику. Полный HTML из пользовательского поля без очистки — это не форматирование, а готовая поверхность атаки.
2. Не смешивайте данные и код
Не помещайте пользовательские строки в onclick, script, CSS-выражения и конструктор функций. Передавайте значения в обработчик как данные, через DOM-свойства или структурированный JSON. Если сервер отдаёт JSON, клиент должен разбирать его как JSON, а не вставлять ответ в HTML.
Автоэкранирование шаблонизатора помогает только в тех местах, которые он поддерживает. Режимы вроде «raw HTML», «safe» или «не экранировать» требуют отдельного аудита: после их появления защита обычно зависит уже от разработчика.
3. Проверяйте URL и атрибуты
Экранирование не превращает любой адрес в безопасный. Для ссылок и редиректов задайте бизнес-правило: какие схемы разрешены, нужны ли внешние домены, можно ли использовать протокол без явного указания. Для изображения проверяйте тип ресурса, для ссылок — отдельно URL, а не только отображаемый текст.
Не используйте пользовательские данные в атрибутах событий. Вместо строки в onclick зарегистрируйте обработчик в JavaScript и храните идентификатор записи в обычном атрибуте или структуре данных.
4. Добавьте Content Security Policy
Content Security Policy, или CSP, — заголовок, который ограничивает источники сценариев и других ресурсов. Он не заменяет экранирование: политика может снизить последствия ошибки, но не исправляет неправильный вывод данных.
Начинайте с режима наблюдения, соберите нарушения, затем переходите к блокирующей политике. Для нового приложения стоит запретить inline-скрипты и не разрешать выполнение строк как кода. Точный набор директив зависит от архитектуры, системы аналитики, CDN и способа загрузки JavaScript.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
Не копируйте этот заголовок без проверки приложения. Если сайт использует inline-скрипты или сторонние виджеты, они перестанут работать. Правильнее составить список источников, проверить отчёты и постепенно убрать исключения, чем добавить широкое разрешение вроде 'unsafe-inline'.
Как выбирать подход
Для обычного пользовательского текста выбирайте текстовый вывод и автоэкранирование. Для ограниченного форматирования — проверенный sanitizer с тестами на разрешённые теги и атрибуты. Для общей защиты приложения добавляйте CSP после инвентаризации скриптов.
| Ситуация | Решение | Цена и риск |
|---|---|---|
| Имя, комментарий, поисковая фраза | Текстовый вывод и автоэкранирование | Минимальная цена; нельзя получить форматирование |
| Редактор с ограниченным HTML | Sanitizer и белый список элементов | Нужны тесты, обновления и контроль конфигурации |
| Много клиентских приложений и виджетов | CSP с узкими источниками и nonce или hash-подходом | Внедрение сложнее; ошибки политики ломают интерфейс |
Что ломается первым
- «Мы экранируем всё». Экранирование применяют не в том контексте или отключают для шаблона с HTML.
- Очистка регулярными выражениями. HTML и URL имеют слишком много вариантов разбора; самописный фильтр быстро пропускает неожиданный случай.
- Безопасный сервер, уязвимый клиент. API возвращает строку корректно, но фронтенд вставляет её через
innerHTML. - Слишком широкая CSP. Разрешения вроде
*и'unsafe-inline'создают видимость защиты и оставляют много путей для выполнения кода. - Доверие к данным из админки. Роль администратора не делает текст безопасным. Сотрудник может вставить данные из письма, импорта или внешней интеграции.
На ревью ищите не только слово XSS. Прослеживайте данные от источника до места вывода: параметр URL, поле API, база, шаблон, DOM. Для каждой точки задавайте два вопроса: кто контролирует значение и в каком контексте браузер его разберёт.
Граничные случаи
Если приложение показывает SVG, MathML, HTML-письма или пользовательские стили, обычные правила для текста уже недостаточны. Эти форматы имеют собственные элементы и атрибуты, поэтому их нужно либо полностью запретить, либо обрабатывать специализированным sanitizer и тестировать отдельно.
Отдельно проверьте загрузку файлов, предпросмотр Markdown, импорт из CSV и интеграции с внешними системами. Данные, пришедшие «из доверенного API», всё равно остаются данными, пока вы не доказали обратное.
Что делать дальше
Составьте карту всех мест, где приложение выводит пользовательские данные, и разделите их по контекстам. Затем замените опасные DOM-операции на текстовый вывод, включите CSP в режиме отчётов и добавьте негативные тесты для комментариев, поиска, URL и админ-панели. После релиза смотрите отчёты CSP, ошибки браузера и подозрительные изменения в исходном коде страниц. Инвалидация сессий и CSRF-защита важны, но это отдельные темы: здесь рассматривается именно предотвращение XSS.
Помощник по курсовой и диплому: Большая работа поэтапно: от плана до защиты
Спросить за 25 ₽Источники: OWASP Cross Site Scripting Prevention Cheat Sheet, MDN: Content Security Policy
Частые вопросы
Достаточно ли экранировать пользовательский ввод на сервере?
Можно ли использовать innerHTML, если строка пришла из API?
Защитит ли Content Security Policy от всех XSS-атак?
Что делать, если в проекте много inline-скриптов?
Как понять, что у меня другая версия браузера или фреймворка и пример не подходит?
Можно ли проверить XSS только ручным тестом?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.