Каждый второй отчёт на HackerOne с пометкой «Resolved» - это XSS. Не потому, что уязвимость сложная, а потому, что разработчики раз за разом недооценивают контексты, в которых пользовательский ввод попадает в DOM. Классический
<script>alert(1)</script> давно не работает на реальных целях: WAF режет его на подлёте, htmlspecialchars экранирует угловые скобки, а CSP блокирует инлайн-скрипты. Но межсайтовый скриптинг от этого не умирает - он мутирует.Здесь я разберу все три типа XSS в веб-приложениях, не пересказывая OWASP-вики (в сотый раз переписывать спеку смысла нет), а через анализ конкретных HTTP-запросов, ответов сервера и поведения DOM. Покажу, как выглядит поиск XSS уязвимостей на реальном пентесте, какие event-handler'ы обходят сигнатурные WAF прямо сейчас и почему даже DOMPurify - золотой стандарт санитизации - оказался дырявым через Prototype Pollution (CVE-2024-45801, CVSS 7.3 HIGH).
Три типа cross-site scripting через призму HTTP-запросов
Русскоязычные материалы обычно описывают три типа XSS абстрактно: «отражённая возвращается в ответе», «хранимая сохраняется на сервере», «DOM-based работает в браузере». Формально верно, но для пентестера бесполезно - нужен разбор на уровне конкретных запросов и точек внедрения.Reflected XSS атака - от GET-параметра до исполнения
Reflected XSS возникает, когда значение из запроса (GET-параметр, POST-поле, HTTP-заголовок) попадает в ответ сервера без обработки. Ключевое слово - «контекст вставки». Один и тот же ввод ведёт себя совершенно по-разному в зависимости от того, куда он приземляется в HTML.Допустим, на целевом сайте есть поиск:
GET /search?q=test. Сервер возвращает строку Результаты по запросу: test внутри тега <h2>. Первый шаг - кинуть через Burp Repeater значение вроде aaa"'<>{} и посмотреть, какие символы проходят. Если в ответе видим неэкранированные <> - это HTML-контекст, и классический вектор через тег сработает.Но чаще бывает иначе. Значение попадает внутрь атрибута:
<input value="ВВОД">. Тут <script> бесполезен - нужно сначала выйти из атрибута через ">, а затем внедрить свой тег. Пейлоад "><img src=x onerror=alert(1)> закрывает атрибут, закрывает тег и создаёт новый элемент с обработчиком события.Третий контекст - JavaScript. Если ввод попадает в конструкцию
var x = 'ВВОД';, нужен выход из строки: ';alert(1)//. Каждый контекст требует своего пейлоада, и именно поэтому слепой фаззинг без анализа ответа - пустая трата времени.По данным YesWeHack, reflected XSS чаще всего находят в формах поиска, формах логина и URL-путях. При эксплуатации reflected XSS атакующий формирует URL с пейлоадом и доставляет его жертве через фишинг. Если сессия на cookie без флага
HttpOnly, простой fetch с document.cookie в параметре отправляет сессионный токен на сервер атакующего. Прямой путь к угону сессии через XSS (Steal Web Session Cookie, T1539 по MITRE ATT&CK).Stored XSS эксплуатация - персистентный вектор
Stored XSS - самый опасный тип, потому что пейлоад сохраняется на сервере и срабатывает у каждого посетителя заражённой страницы. Внедрение вредоносного скрипта идёт через поля, которые ложатся в базу: комментарии, профили пользователей, тикеты поддержки, названия файлов.На практике stored XSS эксплуатация выглядит так. Атакующий находит поле, которое принимает ввод и отображает его другим пользователям. Закидывает туда тег с обработчиком события - например,
<img src=x onerror=alert(document.domain)>. Если при просмотре страницы другим пользователем браузер отрисовывает этот тег и пытается загрузить несуществующее изображение - срабатывает onerror, и JavaScript инъекция исполняется в контексте сессии жертвы.Отдельный подвид - blind XSS. Пейлоад отправляется через форму обратной связи или тикет и срабатывает только в админке, когда сотрудник открывает заявку. Атакующий не видит результат сразу, поэтому использует внешний callback-сервер (Burp Collaborator или свой) для подтверждения срабатывания. Blind XSS особенно ценен на bug bounty - он поражает привилегированных пользователей и часто оценивается как high или critical. Я на одном проекте через blind XSS в тикете поддержки утянул сессию администратора - заявка провисела в очереди три дня, и всё это время пейлоад терпеливо ждал.
DOM-based XSS - когда сервер ни при чём
DOM-based XSS отличается тем, что серверный ответ может быть полностью безопасным - уязвимость кроется в клиентском JavaScript, который берёт данные из контролируемого источника (source) и вставляет их в опасный приёмник (sink).
Типичные source:
location.hash, location.search, document.referrer, window.name, postMessage. Типичные sink: innerHTML, outerHTML, document.write, eval(), setTimeout() со строковым аргументом, jQuery.html().Методика поиска DOM-based XSS сводится к двум шагам. Первый - найти sink в коде. В DevTools на вкладке Sources ищем
innerHTML, eval и другие опасные методы. Второй - проследить, откуда приходят данные. Если цепочка от source до sink не содержит санитизации - это DOM-based XSS.Конкретный cross-site scripting пример: скрипт берёт значение из
window.location.hash через decodeURIComponent, затем вставляет его через innerHTML в элемент на странице. Переход по URL https://target.com/#<img src=x onerror=alert(1)> приводит к исполнению кода без какого-либо взаимодействия с сервером. WAF, серверная валидация - тут бессильны, потому что пейлоад не покидает браузер.В React-приложениях аналогичная проблема возникает при использовании
dangerouslySetInnerHTML с пользовательским вводом. В Angular - при обходе DomSanitizer через bypassSecurityTrustHtml. Современные фреймворки защищают от XSS по умолчанию, но разработчики намеренно отключают защиту ради «гибкости» (и потом удивляются, откуда прилетело).Поиск XSS уязвимостей: пошаговая методология пентеста
Требования к окружению: Burp Suite Community/Pro (актуальная версия), браузер с настроенным прокси (FoxyProxy или системный прокси на127.0.0.1:8080), доступ к целевому приложению с разрешением на тестирование. Для DOM-анализа - Chrome DevTools. Для автоматизированного фаззинга - wordlist с XSS-пейлоадами (рекомендую SecLists xss-payload-list.txt - для начала хватит за глаза).Шаг первый - маппинг поверхности атаки. Проксирую весь трафик через Burp, прохожу по приложению, заполняю все формы, кликаю все ссылки. В Target > Site map вижу полную карту параметров. Каждый параметр, который отражается в ответе - потенциальная точка для reflected XSS. Каждый параметр, который сохраняется и отображается позже - кандидат на stored XSS.
Шаг второй - ручная проверка контекста. Для каждого отражённого параметра отправляю в Repeater тестовую строку со спецсимволами:
'"<>(){}. Смотрю ответ - что экранировано, что осталось. Это определяет вектор атаки. Если <> экранируются, но '" нет, а ввод попадает в атрибут - можно выйти из атрибута и использовать обработчик события без создания нового тега: " onfocus=alert(1) autofocus=". Не торопитесь сразу фаззить - сначала поймите контекст.Шаг третий - фаззинг event-handler'ов. Если стандартные
onerror, onclick, onload блокируются WAF, перебираю менее известные обработчики. Для этого использую PortSwigger XSS Cheat Sheet (актуальная версия 2026 года) - там собраны десятки event-handler'ов с разбивкой по тем, которые требуют и не требуют взаимодействия пользователя. onanimationend, ontransitionend, onscrollend, onbeforematch - всё это реальные способы триггернуть JavaScript без клика.Шаг четвёртый - проверка DOM. Открываю DevTools > Sources, ищу по ключевым словам
innerHTML, document.write, eval, $.html(. Для каждого найденного sink отслеживаю источник данных. Если source контролируем (hash, query, postMessage) - тестирую внедрение через него.Шаг пятый - проверка stored XSS. Вставляю пейлоад в каждое поле, которое сохраняется: имя пользователя, биография, комментарий, имя файла при загрузке. Затем проверяю, как этот ввод отображается - в личном кабинете, в админке, в email-уведомлениях. Blind XSS проверяю через callback на Burp Collaborator: внедряю
<img src=https://COLLABORATOR_ID.burpcollaborator.net> и жду HTTP-обращение.Обход XSS фильтров: от стандартных payload до многоступенчатой доставки
Реальные приложения не стоят голыми перед пентестером. WAF, серверная фильтрация, CSP, санитизаторы - слои защиты, которые нужно обходить последовательно. Именно здесь начинается настоящий XSS пентест.
📚 Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Помимо экзотических event-handler'ов, по данным PortSwigger XSS Cheat Sheet, работают следующие подходы к обходу:
- CSS-анимации как триггер:
onanimationstart,ontransitionendи их webkit-версии срабатывают без пользовательского взаимодействия при наличии CSS-правила с@keyframes - Consuming tags: незакрытые теги, которые «поглощают» часть страницы и меняют контекст парсинга
- Encoding: HTML-entities (
onerror), Unicode-escaped последовательности в JavaScript, двойное URL-кодирование для обхода серверных декодеров - Framework-specific: в Vue.js -
{{constructor.constructor('alert(1)')()}}для template injection; в AngularJS - sandbox escape через$eval
CVE-2024-45801 - когда DOMPurify перестаёт защищать
DOMPurify от Cure53 - стандартная библиотека для санитизации HTML на клиенте. Если приложение используетDOMPurify.sanitize(userInput) перед вставкой в DOM - считается, что XSS невозможен. В документации чёрным по белому написано, что всё безопасно. CVE-2024-45801 доказывает обратное.Уязвимость получила оценку CVSS 7.3 (HIGH) с вектором CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L. Затронуты версии DOMPurify до 2.5.4 и до 3.1.3. Два CWE описывают суть проблемы: CWE-1333 (Inefficient Regular Expression Complexity) - неэффективность проверки глубины вложенности HTML, и CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes) - недостаточная защита от Prototype Pollution.
Атака работает двумя путями. Первый - специально сконструированный HTML с глубокой вложенностью, который обходит depth-checker DOMPurify. Второй - Prototype Pollution: если атакующий может загрязнить прототип
Object (через другую уязвимость в приложении или через gadget), он манипулирует поведением Number.isNaN, от которого зависит проверка глубины. После этого любой вредоносный HTML проходит через sanitize() как нож сквозь масло.Патч оказался элегантным - в коде DOMPurify обернули
Number.isNaN в функцию unapply, которая отвязывает метод от прототипа и делает его невосприимчивым к Prototype Pollution. По данным GitHub Security Advisory (GHSA), помимо CVE-2024-45801 у DOMPurify есть история уязвимостей: GHSA-39q2-94rc-95cp (обход FORBID_TAGS через ADD_TAGS) и GHSA-63q7-h895-m982 (XSS в более ранних версиях, CVE-2020-26870).Для пентестера вывод простой: при обнаружении DOMPurify на целевом приложении - проверяйте версию.
console.log(DOMPurify.version) в DevTools Console (если библиотека доступна глобально) или анализ минифицированного кода на характерные строки покажет, используется ли уязвимая версия. Если приложение пропускает HTML через DOMPurify < 3.1.3 - тестируйте обход через глубокую вложенность и Prototype Pollution.
JavaScript:
// Проверка версии DOMPurify в консоли браузера
console.log(DOMPurify.version);
// Если < 2.5.4 или < 3.1.3 - уязвима к CVE-2024-45801
// Концепция Prototype Pollution gadget:
Object.prototype.__proto__ = { isNaN: () => true };
// После загрязнения прототипа DOMPurify.sanitize() пропускает вредоносный HTML
Маппинг XSS на MITRE ATT&CK: от внедрения до импакта
XSS уязвимость эксплуатация - это неalert(1) ради скриншота. Каждый этап атаки через межсайтовое выполнение скриптов укладывается в конкретные техники MITRE ATT&CK, и понимание этого маппинга критично для написания отчётов и оценки импакта.Initial Access - Exploit Public-Facing Application (T1190). Атакующий эксплуатирует XSS уязвимость веб-приложения для первичного внедрения кода в контекст браузера жертвы. Для reflected XSS нужна доставка URL жертве, для stored - достаточно дождаться посещения заражённой страницы.
Execution - JavaScript (T1059.007). Внедрённый скрипт исполняется движком JavaScript браузера. Атакующий ограничен браузерной песочницей, но внутри неё может выполнять произвольный код.
Credential Access - Steal Web Session Cookie (T1539). Классический угон сессии через XSS: скрипт читает
document.cookie и отправляет его на сервер атакующего. Работает только при отсутствии флага HttpOnly на сессионной cookie. Параллельно - Web Portal Capture (T1056.003): создание фейковой формы логина поверх легитимного приложения для перехвата учётных данных.Collection - Browser Session Hijacking (T1185). Атакующий использует украденную сессию для выполнения действий от имени жертвы: смена email, пароля, вывод данных.
Defense Evasion - Command Obfuscation (T1027.010). Многоступенчатая доставка payload, base64-кодирование, загрузка строк через
fetch - обход WAF и CSP.Impact - External Defacement (T1491.002) и Transmitted Data Manipulation (T1565.002). Подмена контента страницы для фишинга или дефейса, модификация данных форм перед отправкой на сервер.
Такой маппинг в отчёте превращает «XSS в поле поиска» в цепочку атаки с конкретным импактом, что серьёзно влияет на оценку severity при XSS пентесте. Без маппинга - «medium, ну XSS же». С маппингом - «high, потому что T1539 → T1185 → полный захват аккаунта администратора».
Content Security Policy обход: что работает на практике
CSP - последний рубеж обороны, который блокирует исполнение инлайн-скриптов даже при успешной HTML-инъекции. Строгая политикаscript-src 'self' запрещает инлайн-обработчики и сторонние скрипты. Но на практике CSP часто дырявый.Самые частые ошибки конфигурации: наличие
'unsafe-inline' (полностью убивает CSP против XSS), использование 'unsafe-eval' (позволяет eval() в пейлоадах), whitelist-подход с доменами CDN. Последний - моя любимая находка: если cdnjs.cloudflare.com в whitelist, атакующий загружает оттуда уязвимую библиотеку Angular и выполняет произвольный код. Разработчик думал, что разрешил «только CDN», а на деле открыл дверь настежь.Подход с
nonce (script-src 'nonce-R4nd0m') надёжнее, но требует уникального nonce на каждый запрос. Если nonce статичен или предсказуем - CSP обход тривиален. Также если на странице есть JSONP-эндпоинт на whitelisted-домене, атакующий может использовать его как gadget для выполнения кода: <script src="https://whitelisted.com/jsonp?callback=alert"></script>.Порядок действий при аудите CSP: скопировать заголовок
Content-Security-Policy из ответа, прогнать через Google CSP Evaluator (csp-evaluator.withgoogle.com), определить слабые директивы и построить вектор обхода под конкретную политику. Занимает пять минут, а результат бывает неожиданным - на одном проекте CSP Evaluator показал три high-severity проблемы в политике, которую заказчик считал «строгой».Вопрос к читателям
При тестировании WAF-обхода на реальных целях я всё чаще тянусь к event-handler'ам из PortSwigger XSS Cheat Sheet, которые не попали в сигнатуры:onscrollsnapchanging, onbeforeinput, oncontentvisibilityautostatechange. Но у каждого WAF свой набор правил. Какие event-handler'ы срабатывали у вас на конкретных WAF - Cloudflare, ModSecurity с CRS 4.x, AWS WAF? Приведите пейлоад и название WAF: например, <xss onbeforematch=alert(1) hidden=until-found> против Cloudflare Managed Rules - блокируется или проходит в вашем случае?
Последнее редактирование модератором: