XSS: типы и защита
Разбираем три типа XSS на безопасном стенде и учимся закрывать их экранированием вывода, политикой CSP и правильными флагами cookie.
XSS (Cross-Site Scripting) — уязвимость, при которой приложение отдаёт пользовательский ввод в HTML-страницу без обработки, и браузер жертвы исполняет его как код страницы.
Практика идёт на учебных стендах: DVWA содержит отдельные модули Reflected и Stored XSS, в OWASP Juice Shop есть DOM-XSS. Мы изучаем XSS, чтобы защищать свои приложения, а не атаковать чужие: внедрение скрипта на чужой сайт ради кражи сессий — это статьи 272/273 УК РФ. Все примеры — на локальной ВМ.
Зачем это знать защитнику
XSS опасен тем, что код выполняется в браузере другого пользователя, в его сессии. Через XSS крадут токены, выполняют действия от имени жертвы, подменяют содержимое страницы. Разработчик, который понимает три механизма доставки XSS, знает, в каких именно местах кода нужна защита — на выводе данных. Это профилактика, встроенная в каждый шаблон.
Три типа XSS
Reflected (отражённый)
Ввод приходит в запросе и тут же «отражается» в ответе. Уязвимый шаблонизатор без экранирования:
# УЯЗВИМО: ввод вставлен в HTML как есть
name = request.args.get("name", "")
return "<p>Привет, " + name + "!</p>"
Если в name положить строку с тегом <script>...</script>, она попадёт в HTML и браузер её исполнит. Доставка обычно через подготовленную ссылку, по которой кликает жертва. Это разовое, «отражённое» срабатывание.
Stored (хранимый)
Опаснее: вредоносный ввод сохраняется (комментарий, профиль, отзыв) и потом отдаётся всем, кто открывает страницу. Один внедрённый тег бьёт по всем посетителям — это «самораспространяющийся» сценарий на форумах и в чатах. Корень тот же: данные кладутся в HTML без экранирования на выводе.
DOM-based
Сервер ни при чём — небезопасен сам клиентский JavaScript: он берёт данные из URL/хранилища и пишет их в страницу опасным способом:
// УЯЗВИМО: ввод из URL пишется в innerHTML
el.innerHTML = location.hash.slice(1); // hash контролирует пользователь
Свойство innerHTML парсит строку как HTML, поэтому переданная разметка оживает уже в браузере, без участия сервера.
Как это находят на CTF
На стенде исследователь вводит безобидный пробный маркер (например, текст с угловыми скобками) и смотрит в исходник ответа: если скобки пришли «как есть», а не как < — вывод не экранируется, точка потенциально уязвима. Подтверждают безвредным alert(1)-стилем в своей же лаборатории. Цель проверки — найти место без экранирования, чтобы его починить.
Как это работает под капотом
Браузер строит DOM, разбирая HTML. Он не знает, какие символы «от пользователя», — для него <script> в потоке HTML это команда исполнить скрипт. Значит, защита — не дать пользовательским символам попасть в HTML как разметка. Ключевое слово — контекст вывода: одни и те же данные по-разному опасны внутри текста, внутри атрибута, внутри URL и внутри <script>. Защита всегда привязана к месту вывода.
Как защититься
1. Экранирование на выводе — главная мера. Превращайте < > & " ' в HTML-сущности при вставке в страницу. Современные шаблонизаторы (Jinja2, Django templates, React JSX) делают это по умолчанию:
<!-- БЕЗОПАСНО: автоэкранирование шаблонизатора -->
<p>Привет, {{ name }}!</p> <!-- < и > станут < > -->
Опасность возвращается, если экранирование отключить: фильтр |safe в Jinja/Django, v-html во Vue, dangerouslySetInnerHTML в React. Применяйте их только к заведомо доверенному или предварительно очищенному контенту.
2. Для DOM-XSS — безопасные API. Вместо innerHTML используйте textContent (вставляет строку как текст, не как разметку):
// БЕЗОПАСНО: значение вставлено как текст, теги не оживают
el.textContent = location.hash.slice(1);
Если HTML действительно нужен — очищайте его санитайзером (например, DOMPurify) перед вставкой.
3. Content-Security-Policy (CSP) — второй рубеж. Заголовок CSP запрещает выполнение инлайновых и внешних скриптов, кроме явно разрешённых. Даже если XSS просочился, браузер не исполнит чужой скрипт:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
4. HttpOnly и SameSite для cookie — снижают ущерб. Флаг HttpOnly делает cookie недоступной из JavaScript, поэтому даже исполненный XSS не прочитает сессионный токен через document.cookie. SameSite и Secure усиливают защиту:
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
Это слои в глубину. Экранирование предотвращает XSS, CSP ограничивает его исполнение, HttpOnly ограничивает последствия. Полагаться на один слой нельзя.
Защита по контексту вывода
Один и тот же приём экранирования не годится для всех мест страницы — защита зависит от того, куда попадают данные. Внутри текста достаточно экранировать < > &. Внутри значения атрибута добавляются кавычки. Если данные идут в href/src — проверяйте схему URL (разрешайте только http/https, блокируйте javascript:). А вставлять пользовательский ввод прямо в <script> или в обработчик события (onclick) не следует вовсе: это самый опасный контекст. Поэтому правильный шаблонизатор знает контекст и применяет нужное кодирование автоматически — не пытайтесь экранировать вручную «на глаз».
Как проверить защиту в лаборатории
На своём стенде убедитесь, что защита действительно стоит: введите безобидный маркер с угловыми скобками и посмотрите исходник ответа — скобки обязаны прийти как </>, а не как живые теги. Проверьте, что для сессионной cookie в ответе присутствуют флаги HttpOnly и Secure, а заголовок Content-Security-Policy отдаётся и запрещает инлайн-скрипты. Такую проверку полезно вынести в автотест: он поймает регрессию (например, случайно добавленный |safe) раньше, чем уязвимость попадёт в продакшен. Это и есть работа защитника — превратить разовую находку в постоянный контроль.
Итоги
- XSS бывает reflected (отражён из запроса), stored (сохранён и бьёт по всем) и DOM-based (опасный клиентский JS).
- Корень всегда один: пользовательский ввод попадает в страницу как разметка, и браузер исполняет его.
- Главная защита — экранирование на выводе с учётом контекста; современные шаблонизаторы делают это сами.
- Для DOM используйте
textContent/санитайзер вместоinnerHTML; добавьте CSP и HttpOnly/SameSite как рубежи в глубину. - Практика — только на стенде; внедрение скриптов на чужой сайт наказуемо (ст. 272/273 УК РФ).