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

На стенде исследователь вводит безобидный пробный маркер (например, текст с угловыми скобками) и смотрит в исходник ответа: если скобки пришли «как есть», а не как &lt; — вывод не экранируется, точка потенциально уязвима. Подтверждают безвредным alert(1)-стилем в своей же лаборатории. Цель проверки — найти место без экранирования, чтобы его починить.

Как это работает под капотом

Браузер строит DOM, разбирая HTML. Он не знает, какие символы «от пользователя», — для него <script> в потоке HTML это команда исполнить скрипт. Значит, защита — не дать пользовательским символам попасть в HTML как разметка. Ключевое слово — контекст вывода: одни и те же данные по-разному опасны внутри текста, внутри атрибута, внутри URL и внутри <script>. Защита всегда привязана к месту вывода.

Как защититься

1. Экранирование на выводе — главная мера. Превращайте < > & " ' в HTML-сущности при вставке в страницу. Современные шаблонизаторы (Jinja2, Django templates, React JSX) делают это по умолчанию:

<!-- БЕЗОПАСНО: автоэкранирование шаблонизатора -->
<p>Привет, {{ name }}!</p>     <!-- < и > станут &lt; &gt; -->

Опасность возвращается, если экранирование отключить: фильтр |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) не следует вовсе: это самый опасный контекст. Поэтому правильный шаблонизатор знает контекст и применяет нужное кодирование автоматически — не пытайтесь экранировать вручную «на глаз».

Как проверить защиту в лаборатории

На своём стенде убедитесь, что защита действительно стоит: введите безобидный маркер с угловыми скобками и посмотрите исходник ответа — скобки обязаны прийти как &lt;/&gt;, а не как живые теги. Проверьте, что для сессионной cookie в ответе присутствуют флаги HttpOnly и Secure, а заголовок Content-Security-Policy отдаётся и запрещает инлайн-скрипты. Такую проверку полезно вынести в автотест: он поймает регрессию (например, случайно добавленный |safe) раньше, чем уязвимость попадёт в продакшен. Это и есть работа защитника — превратить разовую находку в постоянный контроль.

Итоги

  • XSS бывает reflected (отражён из запроса), stored (сохранён и бьёт по всем) и DOM-based (опасный клиентский JS).
  • Корень всегда один: пользовательский ввод попадает в страницу как разметка, и браузер исполняет его.
  • Главная защита — экранирование на выводе с учётом контекста; современные шаблонизаторы делают это сами.
  • Для DOM используйте textContent/санитайзер вместо innerHTML; добавьте CSP и HttpOnly/SameSite как рубежи в глубину.
  • Практика — только на стенде; внедрение скриптов на чужой сайт наказуемо (ст. 272/273 УК РФ).
Проверьте себя
1. Где должна находиться основная защита от XSS?
AНа выводе данных в HTML — через экранирование с учётом контекста
BНа входе — достаточно один раз отфильтровать запрещённые слова при приёме
CТолько в базе данных — хранить данные в зашифрованном виде
DВ сетевом firewall, который блокирует тег script
2. Чем именно помогает флаг HttpOnly у сессионной cookie при XSS?
AПолностью предотвращает выполнение XSS на странице
BДелает cookie недоступной из JavaScript, поэтому исполненный скрипт не прочитает токен через document.cookie
CШифрует содержимое cookie на стороне сервера
DЗапрещает браузеру строить DOM из ответа