XSS (cross-site scripting) это внедрение скрипта в страницу, которую видит другой пользователь. Скрипт выполняется в контексте вашего сайта: читает данные страницы, делает запросы от имени жертвы, подменяет интерфейс. Это инъекция, только интерпретатор здесь браузер.
Три вида XSS
- Отражённый: вредный ввод приходит в запросе и сразу попадает в ответ.
https://site.ru/search?q=<script>…</script>. Жертву заманивают по ссылке. - Хранимый: ввод сохраняется (комментарий, имя профиля) и показывается всем, кто откроет страницу. Самый опасный.
- DOM-based: уязвимость целиком в клиентском JavaScript: данные из
location.hashилиpostMessageпопадают вinnerHTML,eval,document.write. Сервер может ничего не видеть.
Экранирование по контексту
Главная защита: выводить данные так, чтобы браузер воспринимал их как текст. Правила зависят от контекста:
| Контекст | Что делать |
|---|---|
| Текст внутри HTML | Экранировать & < > " ' в сущности |
| Значение атрибута | Экранировать и всегда брать атрибут в кавычки |
URL в href, src | Проверить схему (только http, https, относительные), затем экранировать |
Внутри <script> и обработчиков onclick | Не вставлять данные; передавать через data- атрибуты или JSON с экранированием |
| CSS | Не вставлять пользовательские значения |
Экранирование HTML не защищает href="javascript:alert(1)": в этой строке нет опасных символов. Поэтому для URL нужна проверка схемы.
Фреймворки
React, Vue, Angular и шаблонизаторы с автоэкранированием (Jinja2, Twig, Thymeleaf) экранируют текст по умолчанию. XSS появляется, когда защиту обходят:
dangerouslySetInnerHTML,v-html,[innerHTML],|safe,{!! !!},th:utext;href={userUrl}без проверки схемы;- прямой доступ к DOM:
el.innerHTML = …; - рендер на сервере с ручной склейкой HTML.
Если нужно показать пользовательский HTML (редактор статей), его пропускают через санитайзер с allowlist тегов и атрибутов, например DOMPurify. Регулярные выражения для этого не годятся.
Content Security Policy
CSP это заголовок, который ограничивает, откуда браузер может загружать и выполнять скрипты. Он не исправляет XSS, а резко снижает последствия. Современная строгая политика на nonce:
Content-Security-Policy: script-src 'nonce-R4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
nonceновый случайный на каждый ответ; выполняются только скрипты с этим атрибутомnonce.'strict-dynamic'разрешает скриптам с nonce подгружать другие скрипты.object-src 'none'отключает плагины,base-uri 'none'не даёт подменить базовый URL.'unsafe-inline'и'unsafe-eval'почти полностью обесценивают политику.- Политики по списку доменов (
script-src cdn.example.com) часто обходятся через JSONP и старые библиотеки на тех же доменах. - Внедрять через
Content-Security-Policy-Report-Only: собрать нарушения, исправить, затем включить.
Другие полезные директивы: frame-ancestors (защита от кликджекинга, замена X-Frame-Options), form-action, upgrade-insecure-requests.
Trusted Types
Директива require-trusted-types-for 'script' запрещает передавать обычные строки в опасные DOM-приёмники (innerHTML, eval). В них можно передать только объект, созданный политикой, где происходит санитизация. Это закрывает DOM XSS на уровне платформы (поддержка в Chromium-браузерах, в других появляется).
Что помогает дополнительно
- Cookie сессии с
HttpOnly: скрипт не прочитает токен (но всё равно сможет делать запросы от имени жертвы). X-Content-Type-Options: nosniffи правильныйContent-Typeдля пользовательских файлов.- Пользовательский контент на отдельном домене (sandbox-домен для загрузок).
Частые ошибки
- «Мы на React, XSS невозможен».
- Экранировать при сохранении в базу, а не при выводе (данные потом идут в разные контексты).
- Санитизировать регулярками.
- CSP с
'unsafe-inline'.
Что спросят на собесе
- Виды XSS и чем DOM XSS отличается от остальных.
- Почему React не защищает от
href="javascript:…". - Как работает CSP с nonce и почему allowlist доменов слабее.
- Защищает ли HttpOnly от XSS.
- Как безопасно показать HTML от пользователя.