Защита от XSS атак: типы, эскейпинг, CSP, Trusted Types
Защита от XSS атак: типы, эскейпинг, CSP, Trusted Types
XSS (Cross-Site Scripting) — исполнение злоумышленником произвольного JavaScript в контексте вашего домена. Последствия: кража session cookies, keylogging, фишинг, полный захват аккаунта. XSS входит в OWASP Top 10: Injection и остаётся в топе находок любого pentest. В статье — три типа XSS, правильный эскейпинг по контексту, CSP и Trusted Types как defense-in-depth.
Три типа XSS
- Stored XSS — полезная нагрузка сохраняется в БД (комментарий, профиль) и выводится всем посетителям. Самый опасный.
- Reflected XSS — полезная нагрузка в URL-параметре и возвращается в HTML ответа. Нужна атака через фишинг-ссылку.
- DOM-based XSS — нагрузка обрабатывается JS на клиенте (
innerHTML,document.write) без участия сервера.
Контекстный эскейпинг
Главное правило: эскейпинг зависит от контекста вывода.
<!-- HTML body -->
<div>{{ escaped_html }}</div> ← &, <, >, ", '
<!-- HTML attribute -->
<img alt="{{ escaped_attr }}"> ← то же + кавычки
<!-- JS string literal -->
var x = "{{ escaped_js }}"; ← экранировать \, ', ", newline, unicode
<!-- URL attribute -->
<a href="{{ url_encoded }}"> ← rawurlencode + whitelist scheme
<!-- CSS value -->
color: {{ css_escaped }}; ← только [a-zA-Z0-9#%]
PHP
function e(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo '<div>', e($userInput), '</div>';
JavaScript (DOM)
// Опасно
element.innerHTML = userInput;
// Безопасно
element.textContent = userInput;
// Если нужен HTML — используйте DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
React / Vue
React по умолчанию эскейпит содержимое {variable}. Опасна только конструкция dangerouslySetInnerHTML и нединамические href:
// ❌
<a href={userInput}> ... // javascript: схема
// ✅
const safeHref = userInput.startsWith('http') ? userInput : '#';
<a href={safeHref}>
CSP как последняя линия
Даже идеальный эскейпинг не спасёт от нулевого-дня в фреймворке. CSP превращает успешный XSS в заблокированный скрипт. Минимум:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM' 'strict-dynamic'; object-src 'none'; base-uri 'self'
Детали — в CSP setup.
Trusted Types (Chrome 83+)
Trusted Types — механизм, который заставляет все DOM sink'и (innerHTML, script.src) принимать только «доверенные» строки, прошедшие через policy-функцию. Значительно уменьшает DOM-based XSS:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default
trustedTypes.createPolicy('default', {
createHTML: (s) => DOMPurify.sanitize(s),
});
element.innerHTML = 'raw html'; // TypeError без policy
HTTP-only cookies
Флаг HttpOnly не даёт JS читать cookie — даже при успешном XSS session не утечёт. Обязательно вместе с Secure и SameSite. Детали — Cookie Security.
Server-side проверки
- WAF (mod_security rules, Cloudflare) отловит базовые payload <script>
- Валидация входа по whitelisting (email, URL, число)
- Content-Type: application/json для JSON API документацию — не text/html
- Санитизация HTML (TinyMCE, rich editors) — HTMLPurifier на PHP, DOMPurify на JS
Проверка
Тест-payload: <img src=x onerror="alert(1)">. Если алерт сработал в браузере — у вас XSS. Статический анализ — Semgrep, Snyk Code, SonarQube. Регулярный аудит security headers через Security Scanner.
FAQ
Достаточно ли strip_tags в PHP? Нет — обходится через <svg>, onerror, HTML-entities. Только htmlspecialchars для body и DOMPurify/HTMLPurifier для rich-content.
Защищает ли SSL/TLS проверку от XSS? Нет — XSS живёт внутри исполненного JS на легитимном домене.
jQuery .html() опасен? Да — это обёртка вокруг innerHTML. Используйте .text().
Что с Markdown-инпутом? Парсите в sanitized HTML через marked + DOMPurify, не пропускайте raw HTML.
Вывод
Слоёная защита: контекстный эскейпинг + CSP + Trusted Types + HttpOnly cookies + WAF. Мониторьте CSP-отчёты через мониторинг и проверяйте конфигурацию в Security Scanner. Связанное чтение: CSP Setup, Cookie Security.
Проверьте ваш сайт прямо сейчас
Проверить →