Content Security Policy - это не серебряная пуля. Это швейцарский нож с тупым лезвием и кривой отвёрткой, который тебе вручили, чтобы ты отбивался от танка.
CSP - это не защита. Это - механизм контроля. И в этом заключается вся его фундаментальная уязвимость. Его продают как неприступную стену, но на деле это - длинный, сложный, зачастую написанный дрожащей рукой список правил для очень буквоядного, но не очень умного охранника (браузера). А где есть длинные правила, там есть противоречия. Где есть буквоядность, там есть злоупотребление буквой. Где есть сложность, там - ошибки.
CSP - это политика. Политику можно интерпретировать. Ею можно манипулировать. Её можно саботировать изнутри с помощью её же собственных механизмов.
А теперь представь, что эта хрупкая, переусложнённая система контроля построена поверх другой системы - древней, архаичной, работающей на принципах доверия полувековой давности. Эта система - DNS. Та самая, которую все воспринимают как данность, как воздух. «Ну, DNS же просто превращает exаmple.com в 93.184.216.34, что тут может пойти не так?»
Вот именно так и начинаются самые красивые катастрофы. Слабейшее звено в цепи безопасности - это всегда то, которое все считают надёжным. Пока фронтенд-разработчик бьётся над каскадными стилями и скриптами, пока бэкендер оптимизирует запросы к базе, целый пласт атак существует на уровне, который им невидим - на уровне разрешения имён, TTL-таймаутов, кеширования, поддоменов и CNAME-цепочек. И CSP, при всей своей сложности, слепо доверяет этому пласту. Если в политике сказано
script-src https://*.trustеd-cdn.net, то браузер не будет проводить расследование, кому на самом деле принадлежит IP за malicious-subdоmain.trustеd-cdn.net. Он спросит DNS, получит ответ и доверчит.Сегодня мы не будем скользить по поверхности. Мы наденем гидрокостюмы и нырнём в самые тёмные, илистые воды на стыке двух протоколов: высокоуровневой политики CSP и низкоуровневой, фундаментальной системы DNS.
Дисклеймер (или, как я это называю, «ритуал отведения юридической порчи»):
Всё, что будет описано в этом материале, имеет строго одну цель - углублённое изучение механизмов безопасности для их усиления. Это инструмент для:
- Тестирования своих собственных ресурсов (у тебя есть на них все права).
- Проведения легальных пентестов в рамках ясного, письменного, подписанного соглашения с владельцем системы.
- Понимания векторов атак, чтобы строить более robust-ную защиту.
Итак, если ты готов запачкать руки конфигами, ошибаться в стендах, дебажить скрипты и думать на три слоя глубже маркетинговых слоганов - начинаем наше погружение. Первая цель - разобрать, почему CSP, этот якобы вершитель судеб, на самом деле так сильно зависит от старого, доброго и очень уязвимого DNS.
Часть 1: CSP - не священный Грааль, а инструкция с дырками
Для начала давайте без иллюзий. Что такое CSP? По сути, это такой HTTP-заголовок:Content-Security-Policy: default-src 'self'; script-src 'self' https://trustеd.cdn.com;Браузер читает это и говорит: «Окей. Скрипты можно грузить только с того же источника (
'self') и с https://trustеd.cdn.com. Всё остальное - блокирую».Кажется, что это убивает классические XSS? Как бы не так. Основные проблемы:
- Политики можно ослаблять. Разработчикам лень, им надо встроить виджет из десятка источников. В итоге политика обрастает директивами вроде
script-src *,unsafe-inlineилиhttps:. Это грубейшие ошибки, но они повсеместны. - Динамический код. Директива
'unsafe-eval'разрешаетeval(),setTimeout('string')и т.д. Многие фреймворки (старые версии Angular, некоторые настройки React) требуют этого. Это сразу огромная дыра. - Унаследованный код. Самая частая история: «У нас на проекте 15 лет legacy, мы не можем всё переписать, просто включите CSP, чтоб аудитор отстал». Включают максимально разрешающую политику, которая лишь создаёт видимость безопасности.
Код:
Content-Security-Policy: default-src 'none';
script-src 'self' https://assets.myapp.com;
style-src 'self';
img-src 'self' https://images.myapp.com;
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
Смотрится грозно.
'self' - это только текущий протокол, хост и порт. Плюс явно указанный CDN для скриптов и картинок. Где тут атаковать? Кажется, что нигде.Вот тут-то и начинается магия происхождения (origin).
Что такое
'self' - это текущий origin. Origin - это схема (протокол), хост (домен) и порт. https://myаpp.com:443 и https://myаpp.com - один origin. http://myаpp.com (HTTP) и https://myаpp.com (HTTPS) - разные origins. https://myаpp.com и https://www.myаpp.com - разные origins! Это важно.А что такое хост? На уровне браузера и HTTP - это просто строка. То, что мы, люди, читаем как «поддомен.домен.зона» - для браузера сначала просто текст. Превращение этого текста в IP-адрес - задача DNS. И вот здесь лежит первая группа лазеек.
Часть 2: DNS - забытый картограф в мире политик
Все думают про DNS как про телефонную книгу. Ввёл имя - получил IP. Скучно. Но в контексте безопасности веба DNS - это система, определяющая границы доверия. Если CSP говорит'self', браузер смотрит на свой текущий origin (например, https://app.mуcompany.com) и разрешает ресурсы с того же хоста.А что, если хост может меняться? Что если app.mycompany.com в один момент ведёт на
IP 10.0.0.1, а через минуту - на 10.0.0.2? Это же всё ещё 'self'! А что если злоумышленник может повлиять на это преобразование?Техника 1: Поддомен-призрак (Subdomain Takeover + CSP)
Это классика, но её часто упускают из виду в контексте CSP.Сценарий:
- Компания
myаpp.comиспользует облачный сервис (AWS S3, Heroku, GitHub Pages, Azure Blob Storage) для хостинга статики по адресу assets.myapp.com. - Политика CSP включает
script-src 'self' https://аssets.myapp.com. - Позже компания отказывается от этого сервиса, удаляет ресурсы, но забывает удалить запись CNAME в DNS, которая указывает
аssets.myapp.comна внешний сервис (например,myаpp.herokuapp.com). - Злоумышленник регистрирует этот самый внешний сервис (например, создаёт приложение на Heroku с именем
myаpp).
аssets.myapp.com технически принадлежит атакующему. Он может загрузить туда любой JavaScript-файл. А так как CSP явно разрешает https://аssets.myapp.com, браузер жертвы, зашедшей на https://аpp.myapp.com, доверяет скрипту с этого поддомена и выполняет его. XSS достигнут.Почему это работает против 'self'? Потому что для браузера
аssets.myapp.com - это не 'self' (если основной сайт на app.myаpp.com), но это явно прописанный разрешённый источник в директиве script-src. Забытый поддомен стал троянским конём.Практический инструмент 1: Subjack / SubOver / Can-I-Take-Over-XYZ
Для поиска таких забытых поддоменов есть отличные инструменты.
- Subjack (Go):
go install github.cоm/haccer/subjack@latest. Он берёт список поддоменов и проверяет их на предмет возможности захвата, проверяя «мёртвые» CNAME. - Как использовать в связке с CSP-аудитом:
- Соберите все домены и поддомены цели (
amass,subfinder,assetfinder). - Вытащите политику CSP с главной страницы (можно глазами, можно скриптом).
- Выпишите все разрешённые домены из директив
script-src,style-src,img-src,font-src,connect-src. - Пропустите эти домены через
subjack. Если какой-то из них «мёртвой хваткой» висит на внешнем сервисе - у вас потенциальный вектор.
- Соберите все домены и поддомены цели (
Bash:
# Примерный пайплайн
echo "myapp.com" | subfinder -silent | httprobe -c 50 | while read url; do
curl -sI "$url" | grep -i "content-security-policy" | head -1 > policy.txt
# (парсим policy.txt, вытаскиваем домены)
done
# Допустим, нашли assets.myapp.com
subjack -w domains.txt -t 100 -timeout 30 -ssl -c /path/to/fingerprints.json -o takeover_results.txt
Техника 2: Динамические поддомены и Wildcard DNS
Более интересный случай. Многие SaaS-платформы и современные приложения используют шаблоны вида{user-id}.myаpp.com или {project}.myаpp.com. Часто для этого в DNS прописывается запись *.myаpp.com (wildcard), которая указывает на один и тот же сервер или балансировщик. Сервер, получив запрос на evil.myаpp.com, смотрит на заголовок Host: evil.myаpp.com и решает, что ему показывать.Уязвимость: А что если приложение, работающее на основном origin myapp.com, имеет политику CSP с
script-src 'self'? Кажется, безопасно.Но что такое
'self' для страницы, открытой на аlice.myapp.com? Это origin https://аlice.myapp.com. А что насчёт bоb.myapp.com? Это другой origin! Схема и порт те же, а хост другой. Поэтому для страницы на аlice.myapp.com скрипт с bоb.myapp.com не является 'self' и будет заблокирован CSP.А где же уязвимость? Она - в логике бэкенда и конфигурации
wildcard. Предположим, есть функция, которая позволяет пользователям загружать аватарки. Она сохраняет их по пути /uploads/avatar/{user-id}.jpg и отдаёт по URL https://myаpp.com/uploads/avatar/{user-id}.jpg. CSP с img-src 'self' это разрешит. Всё ок.Но что если разработчик, чтобы «ускорить отдачу статики», решает отдавать аватарки через тот же динамический поддомен? Или что если из-за конфигурации
nginx/Apache файл, доступный по https://myаpp.com/uploads/avatar/evil.jpg, также доступен поhttps://аnything.myapp.com/uploads/avatar/evil.jpg? Потому что wildcard DNS ведёт на тот же сервер, а веб-сервер не проверяет заголовок Host для статических файлов.Теперь у нас есть страница на
https://аlice.myapp.com/ с CSP script-src 'self'. Мы не можем внедрить скрипт. Но можем ли мы внедрить что-то, что заставит страницу загрузить скрипт с origin, который считается 'self'?Да. Например, через уязвимость в загрузке файлов. Если мы можем загрузить файл с расширением
.js и он будет доступен по https://аlice.myapp.com/uploads/temp/our.js - это 'self', CSP пропустит. Но часто загрузка скриптов запрещена. А что с JSONP-эндпоинтами или старыми API, которые возвращают JavaScript? Если они находятся на том же origin и не имеют защитных заголовков, их можно использовать для утечки данных.Ключевой момент: В условиях wildcard DNS аlice.myapp.com и myаpp.com - это разные origins с точки зрения браузера, но один и тот же сервер с точки зрения бэкенда. Это несоответствие можно использовать для:
- Обхода
frame-ancestors. Если myapp.com запрещает встраивание (frame-ancestors 'none'), страницу всё равно можно встроить вiframeсsrc=alice.myаpp.com, если тот же самый контент доступен по этому поддомену (частая ошибка конфигурации). - Загрязнения origin. Если на
evil.myаpp.com(который указывает на тот же IP) мы можем разместить вредоносный скрипт, и найдётся на основном сайте функционал (например, импорт скриптов по URL, который не проверяет полное соответствие origin, а только принадлежность к домену), то CSP может быть обойдён.
Напишем простой, но мощный скрипт на Python, который будет проверять, доступен ли один и тот же контент с разных поддоменов, и как на это реагирует CSP.
Python:
#!/usr/bin/env python3
import requests
from urllib.parse import urlparse
import sys
def check_origin_bypass(target_url, subdomains_file):
"""
Проверяет, доступен ли контент с основного домена
через различные поддомены (wildcard/dynamic).
"""
try:
# Получаем ответ с основного домена
main_response = requests.get(target_url, timeout=10)
main_content = main_response.content[:5000] # Берём первые 5кб для сравнения
main_csp = main_response.headers.get('Content-Security-Policy', '')
print(f"[+] Main URL: {target_url}")
print(f"[+] Main CSP: {main_csp}")
parsed_url = urlparse(target_url)
main_domain = parsed_url.netloc
scheme = parsed_url.scheme
with open(subdomains_file, 'r') as f:
subdomains = [line.strip() for line in f if line.strip()]
for sub in subdomains:
test_domain = f"{sub}.{main_domain}" if sub else main_domain # для @ записи
test_url = f"{scheme}://{test_domain}{parsed_url.path}"
try:
test_response = requests.get(test_url, timeout=10, headers={'Host': main_domain}) # Иногда нужно подменить Host
test_content = test_response.content[:5000]
if test_response.status_code == 200:
# Проверяем, тот же ли контент
if main_content == test_content:
print(f"[!] IDENTICAL CONTENT: {test_url}")
# Проверяем CSP для этого поддомена
test_csp = test_response.headers.get('Content-Security-Policy', '')
if test_csp != main_csp:
print(f" CSP DIFFERENCE! Main: '{main_csp}' vs Sub: '{test_csp}'")
# Анализируем, ослабляет ли это CSP
# (например, если main CSP имеет frame-ancestors 'none', но на поддомене его нет)
if "frame-ancestors 'none'" in main_csp and "frame-ancestors 'none'" not in test_csp:
print(f" [CRITICAL] frame-ancestors policy missing on subdomain! Clickjacking possible.")
else:
# Контент разный, но может быть полезно
print(f"[~] Different content on: {test_url} ({len(test_content)} bytes)")
except requests.exceptions.RequestException as e:
# print(f"[-] Failed to reach {test_url}: {e}")
pass
except Exception as e:
print(f"[-] Global error: {e}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <target_url> <subdomains_list_file>")
sys.exit(1)
check_origin_bypass(sys.argv[1], sys.argv[2])
Этот скрипт - основа. Его можно расширять: проверять не только главную страницу, но и ключевые эндпоинты (
/api, /upload, /static/), искать файлы, которые доступны с любого поддомена, и автоматически сравнивать CSP-заголовки.Часть 3: Глубокая настройка DNS - TTL, кеширование и race conditions
Теперь переходим к более эзотеричным, но от того не менее опасным вещам. DNS - это распределённая кеширующая система. У каждой записи есть TTL (Time To Live) - время, на которое резолверы (провайдеры, браузеры, ОС) могут запомнить ответ.Сценарий Race Condition на основе DNS.
Представьте приложение, которое динамически генерирует уникальные поддомены для сессий или документов:
{random-id}.dоcs.myapp.com. Каждый поддомен указывает (через CNAME или A-запись) на уникальный бэкенд-сервер или путь. CSP на основном сайте myapp.com включает в script-src шаблон *.dоcs.myapp.com (что уже рискованно).- Атака: Злоумышленник инициирует создание «документа» и получает поддомен
sеcret.docs.myapp.com. - Уязвимость: DNS TTL для
*.dоcs.myapp.comустановлен очень низким (например, 1 секунда) для гибкости. - Эксплуатация: Атакующий быстро (в течение окна TTL) меняет DNS-запись для
secret.dоcs.myapp.com, чтобы она указывала на его сервер. Но не просто меняет, а предвосхищает запрос жертвы. - Механика: Атакующий заставляет жертву (через XSS или просто переходя по ссылке) обратиться к
secret.dоcs.myapp.com. Из-за низкого TTL локальный DNS-резолвер жертвы может не иметь записи в кеше и пойдёт запрашивать её заново. Если атакующий сможет «подсунуть» свой IP-адрес быстрее легитимного DNS-сервера (например, контролируя локальный сетевой трафик жертвы или используя уязвимости в DNS-резолверах), то браузер жертвы подключится к серверу злоумышленника. - Итог: Браузер считает, что загружает контент с
secret.dоcs.myapp.com- разрешённого источника согласно CSP. Но на самом деле контент приходит с враждебного сервера, который может отдать вредоносный скрипт. CSP пройден.
Практический инструмент 3: DNS Cache Snooping & Poisoning Test
Мы не будем писать полноценный инструмент для poisoning (это отдельная большая тема), но можем создать скрипт для оценки рисков.
Python:
#!/usr/bin/env python3
import dns.resolver
import time
def check_dns_ttl_and_cache(domain):
"""Проверяет TTL записей A и CNAME для домена и его поддоменов."""
resolver = dns.resolver.Resolver()
# Можно указать конкретный резолвер: resolver.nameservers = ['8.8.8.8']
record_types = ['A', 'CNAME']
for rtype in record_types:
try:
answers = resolver.resolve(domain, rtype)
for answer in answers:
print(f"[+] {rtype} record for {domain}: {answer.to_text()}")
# TTL - это атрибут ответа
print(f" TTL: {answer.ttl} seconds ({answer.ttl/60:.2f} minutes)")
if answer.ttl < 300: # Меньше 5 минут
print(f" [WARNING] Very low TTL ({answer.ttl}s). Potential for race conditions/rapid takeover.")
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
pass
def generate_subdomains(base_domain, wordlist):
"""Генерирует список поддоменов для проверки."""
# Здесь можно реализовать логику подбора или взять готовый список
return [f"{word}.{base_domain}" for word in wordlist[:100]] # Ограничим для примера
if __name__ == "__main__":
target = "myapp.com"
# Проверяем основной домен и потенциально опасные поддомены
check_dns_ttl_and_cache(target)
check_dns_ttl_and_cache(f"*.{target}") # Запись wildcard
check_dns_ttl_and_cache(f"assets.{target}")
check_dns_ttl_and_cache(f"cdn.{target}")
# Можно автоматизировать для списка
# common_subs = ['assets', 'cdn', 'static', 'media', 'uploads', 'api', 'app', 'dev', 'test']
# for sub in common_subs:
# check_dns_ttl_and_cache(f"{sub}.{target}")
Низкий TTL - это палка о двух концах. С одной стороны, быстрее обновление при миграции. С другой - большая поверхность для атак, связанных с кешем.
Часть 4: Атаки на уровень протокола - DNS Pinnging и HTTPS?
Современный веб - это HTTPS. CSP без HTTPS не имеет особого смысла. Но как DNS взаимодействует с HTTPS? Через сертификаты.Когда браузер идёт на
https://аssets.myapp.com, он:- Резолвит DNS, получает IP.
- Устанавливает TLS-соединение с этим IP.
- Проверяет сертификат, который должен быть валидным для
аssets.myapp.com.
Если в политике CSP указан источник
https://аssets.myapp.com, браузер доверяет ему только если соединение установлено с валидным HTTPS. Но что если:- Компания использует wildcard-сертификат
*.myаpp.com. - Этот сертификат и его приватный ключ слишком широко распространены (например, есть на всех серверах разработки, в CI/CD).
- В результате компрометации одного из серверов разработки злоумышленник получает приватный ключ.
IP 1.2.3.4, прописать в своём контролируемом DNS (или через подмену у жертвы) запись аssets.myapp.com; 1.2.3.4, и использовать украденный wildcard-сертификат для валидного TLS-рукопожатия. Браузер жертвы увидит валидный HTTPS для нужного домена и загрузит скрипт. CSP снова обойдён.Защита: Certificate Transparency (CT) и CAA-записи. Но кто их проверяет в реальном времени? Крайне мало кто. Это атака высокого уровня, но она показывает: безопасность CSP упирается в безопасность всей цепочки инфраструктуры, включая управление сертификатами.
Практический инструмент 4: Проверка безопасности поддоменов из CSP
Напишем скрипт, который для каждого домена из CSP проверит:
- Наличие wildcard-сертификата.
- Наличие CAA-записей (кто может выпускать сертификаты).
- Риск Subdomain Takeover.
Python:
#!/usr/bin/env python3
import ssl
import socket
import dns.resolver
from cryptography import x509
from cryptography.hazmat.backends import default_backend
def get_certificate(hostname, port=443):
"""Извлекает сертификат с хоста."""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert_bin = ssock.getpeercert(True)
if cert_bin:
cert = x509.load_der_x509_certificate(cert_bin, default_backend())
return cert
return None
def check_certificate_details(domain):
"""Анализирует сертификат."""
print(f"\n[+] Checking certificate for: {domain}")
try:
cert = get_certificate(domain)
if cert:
# Проверяем Subject Alternative Names (SANs)
try:
san_ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
sans = san_ext.value.get_values_for_type(x509.DNSName)
print(f" SANs: {sans}")
if f'*.{domain.split(".", 1)[-1]}' in sans: # Упрощённо
print(f" [INFO] Wildcard certificate in use.")
except x509.ExtensionNotFound:
pass
# Issuer
issuer = cert.issuer.rfc4514_string()
print(f" Issuer: {issuer}")
# Срок действия
print(f" Valid from: {cert.not_valid_after_utc}")
except Exception as e:
print(f" [-] Could not retrieve certificate: {e}")
def check_caa_record(domain):
"""Проверяет CAA записи."""
try:
answers = dns.resolver.resolve(domain, 'CAA')
for answer in answers:
print(f" CAA Record: {answer.to_text()}")
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
print(f" [WARNING] No CAA records found. Anyone could potentially issue a certificate (with CA that doesn't require CAA).")
except Exception as e:
print(f" [-] Error checking CAA: {e}")
def audit_csp_domains(csp_header):
"""Примитивный парсер CSP и аудит доменов из script-src, style-src и т.д."""
# Пропустим парсинг для краткости. Предположим, у нас есть список domains_from_csp
domains_to_check = ['assets.myapp.com', 'cdn.myapp.com'] # Пример
for domain in domains_to_check:
check_certificate_details(domain)
check_caa_record(domain)
# Здесь же можно интегрировать проверку на Subdomain Takeover (Tool 1)
if __name__ == "__main__":
# Пример вызова
csp_example = "script-src 'self' https://assets.myapp.com https://cdn.myapp.com; style-src 'self';"
audit_csp_domains(csp_example)
Этот скрипт даёт понимание, насколько защищены домены, указанные в CSP как доверенные. Отсутствие CAA и наличие wildcard-сертификата - это факторы риска.
Часть 5: Абсолютно практическое руководство - от разведки до эксплуатации
Давай соберём всё в единый пайплайн для тестирования на реальном объекте (на легитимном, помнишь?).Этап 0: Разведка.
- Сбор поддоменов: Используй
amass, subfinder, assetfinder, shuffledns. Собери всё.
Bash:subfinder -d myapp.com -silent -o subdomains.txt amass enum -passive -d myapp.com -o amass_subs.txt cat subdomains.txt amass_subs.txt | sort -u > all_subs.txt
- Получение CSP: Просканируй найденные живые поддомены и главный домен на наличие CSP.
Bash:cat all_subs.txt | httpx -silent -ports 80,443,8080,8443 -threads 100 | while read url; do echo -n "$url : " ; curl -sI "$url" | grep -i "content-security-policy" | head -1 || echo "NO CSP" done > csp_inventory.txt
- Выбери основную цель (чаще всего это главное приложение
аpp.myapp.com). - Вручную изучи её CSP. Выпиши все директивы, особенно
script-src, style-src, img-src, font-src, connect-src, frame-ancestors. - Составь список всех указанных источников (URL, домены). Обрати внимание на
'self', 'unsafe-inline', 'unsafe-eval', https:.
Для каждого домена из CSP (кроме 'self'):
- Захват поддоменов: Прогони через
subjack/SubOver. - DNS-анализ: Проверь TTL, наличие wildcard (
dig A *.assets.myаpp.com). - HTTPS-анализ: Проверь сертификат (wildcard? срок?), CAA-записи.
- Доступность контента: Используй наш Инструмент 2, чтобы проверить, доступен ли один и тот же контент с разных поддоменов (особенно если в CSP есть
'self'). Это может выявить ошибки конфигурации веб-сервера.
Даже с жёсткой CSP нужно искать, куда можно вставить свой код или повлиять на загрузку ресурсов.
- Поиск JSONP: Найди старые эндпоинты API, которые возвращают JavaScript с callback-параметром.
Bash:# Используем waybackurls, gau echo "myapp.com" | gau | grep -i "callback=" | head -20 echo "myapp.com" | waybackurls | grep -i "jsonp" | head -20
- Загрузка файлов: Найди функционал загрузки аватар, документов, медиа. Можно ли загрузить
.html, .svg(который может содержать скрипт)? Можно ли определить конечный путь к загруженному файлу? Будет ли он доступен с origin'self'?
- Динамические редиректы: Параметры типа
?redirect=, ?next=, ?url=. Могут ли они вести на подконтрольный тебе поддомен, который считается доверенным? Может ли это обойти connect-src или frame-ancestors?
Предположим, мы нашли:
- Политика:
script-src 'self' https://cdn.myаpp.com. - На
cdn.myаpp.comнетSubdomain Takeover, стоит валидный сертификат. - Но мы нашли, что на основном домене есть функционал загрузки изображений, который не проверяет тип файла и сохраняет его по предсказуемому пути:
/uploads/{timestamp}_{filename}. - Файлы из
/uploads/доступны с любого поддомена из-за wildcard DNS и одинаковой конфигурации nginx.
- Создаём файл exploit.jpg, который на самом деле содержит JavaScript. Но браузер не исполнит .jpg как скрипт.
- Нам нужно заставить страницу загрузить наш файл как скрипт. Для этого можно попробовать:
- Уязвимость в загрузке с изменением расширения: Если сервер переименовывает файлы, добавляя своё расширение, это сложно.
- Path Traversal в имени файла: Если можно загрузить файл как
exploit.jpg%2f..%2f..%2fexploit.js, и сервер обработает это, сохранив в/uploads/exploit.js. Маловероятно, но стоит проверить. - Использование base-uri: Если директива
base-uriне настроена или слаба, можно попробовать внедрить
base href="https://attacker.com/"; и затем подгружать скрипты относительно этой базы.
Но это заблокирует CSP, еслиbase-uri 'self'. - Самый реалистичный сценарий для нашего случая - найти возможность внедрить разметку, которая загрузит наш файл не как скрипт, а как данные, и затем выполнить его через другую уязвимость (например, через
eval()в другом доверенном скрипте, если естьunsafe-eval). Без unsafe-eval сложно.
Часть 6: Защита - как не стать жертвой
Если ты админ или разработчик, что делать?- CSP Report-Only и мониторинг. Всегда начинай с
Content-Security-Policy-Report-Only. Настройreport-uriилиreport-toи смотри, что блокируется в реальной работе приложения. Постепенно ужесточай политику. - Конкретные домены, никаких wildcard в источниках. Избегай
*.myаpp.comв директивахscript-src. Указывай явноassets.myаpp.com, cdn.myapp.com. Чем меньше поверхность, тем лучше. - Управляй своими поддоменами. Регулярно аудитуй DNS-записи. Удаляй неиспользуемые CNAME. Используй сервисы вроде AWS Route53, которые могут предотвращать захват поддоменов (путем проверки существования цели CNAME).
- Правильные настройки веб-сервера. Конфигурация
nginx/Apacheдолжна строго учитывать заголовокHost. Блокируй доступ к статическим файлам, если Host не соответствует ожидаемому. Используй разные домены/поддомены для пользовательского контента и системных ресурсов (принцип разделения). - Безопасность инфраструктуры. Wildcard-сертификаты - зло. Используй короткоживущие сертификаты, выпускаемые автоматически (например, через Let's Encrypt и cert-manager в Kubernetes). Настрой CAA-записи. Контролируй доступ к приватным ключам.
- Хеши и nonce. Для современных приложений используй
script-src 'nonce-{random}'. Это самый сильный механизм. Каждый ответ сервера генерирует уникальный nonce, встраивает его в тегscript nonce="{random}";и в заголовок CSP. Даже если атакующий сможет внедрить скрипт, он не сможет угадать nonce для этого запроса. Это убивает практически все рассмотренные выше атаки, связанные с загрузкой скриптов с поддоменов. - strict-dynamic. Современная директива, которая позволяет доверять скриптам, загруженным другим доверенным скриптом (например, загрузчикам библиотек). При грамотном использовании сильно упрощает CSP, переходя от белого списка доменов к модели доверия, основанной на цепочке загрузки.
Код:
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{RAND}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
report-uri /csp-report-endpoint;
А все сторонние сервисы загружай через прокси-эндпоинт на своём origin, тем самым сводя количество доверенных сторонних доменов к нулю.
Заключение
Вот мы и добрались до финала. Но если ты думаешь, что это просто точка в конце статьи, ты сильно ошибаешься. Это - точка сборки. Место, где все разрозненные техники, куски кода и наблюдения должны сложиться в единую философию подхода. В оружие, которое ты положишь в свой арсенал не для галочки, а для реального понимания. Давай пройдемся по этому финальному аккорду медленно, с расстановкой, выжмем из каждой мысли все соки.CSP - Это Не Стена. Это Сложно Настроенная, Глючная Сигнализация с Севшим Аккумулятором
Давай начистоту: индустрия продает тебе CSP как неприступный периметральный забор из умных слов. strict-dynamic, nonce, hash. Звучит солидно. На деле же развертывание CSP в 95% случаев - это не стратегическая установка защиты. Это отчаянная попытка заглушить вой сирены в панели мониторинга, кричащей о бесконечных violation reports.Представь себе не стену, а стареющую систему безопасности в огромном, заброшенном складе. В ней:
- Камеры (script-src, style-src, img-src) - это директивы. Одни смотрят только в коридор, другие имеют слепые зоны за колоннами (те самые
unsafe-inline, которые не смогли убрать, потому что легаси-код развалится). Их углы обзора определяются политикой. - Датычи движения на окнах (connect-src, font-src) - это второстепенные директивы, которые часто настраивают по остаточному принципу, забывая, что окно - тоже путь внутрь.
- Сигнализация (report-uri / report-to) - это механизм отчетности. Она орет, когда что-то ломает правило. Но что, если охранник, который должен реагировать на этот вой, давно выключил звук, потому что 99% срабатываний - это кривые скрипты Яндекс.Метрики? Сигнализация есть, реакции нет. Это и есть типичный статус-кво.
- Аккумулятор всей системы - это её реализация в браузере. Баги в парсере политик, тонкости обработки
default-src, наследование вiframe- это то, что может разрядить всю систему в ноль, превратив её в бесполезную коробку.
- Найти слепые зоны камер. Это те самые DNS, конфигурации серверов, история доменов.
Камера смотрит на доменаssets.cloud-provider.comи считает его безопасным. Но она не видит, что за этим доменом стоит цепочка CNAME наclоud-provider.s3.amazonaws.com, аbucket nameможет быть подобран. Она не видит, что TTL записи - 5 секунд, и в момент между ее кешированием в резолвере и следующим запросом можно подсунуть свой IP. Она слепа к инфраструктуре. - «Подкупить охранника» - это про логику приложения. Самый изящный обход. Зачем ломать правило, если можно заставить легитимный, доверенный компонент сделать грязную работу за тебя?
- Уязвимость JSONP-эндпоинта в доверенном домене, который есть в
script-src. Он же доверенный! Он может вернуть тебе данные с краем чувствительной информации и выполниться. - Возможность загрузить SVG с вредоносным JavaScript в
img-src(в некоторых браузерах при определенных условиях это еще возможно). - Динамическое разрешение путей в доверенных CDN, где можно, оставаясь в рамках разрешенного источника
https://cdn.cоm/, добраться до пользовательского контента:/uploads/../malicious.js. - Это не взлом политики. Это кооптация её элементов. Охранник (браузер) выполняет инструкцию (политику), а ты используешь легальный инструмент (доверенный скрипт) для удара в спину.
- Уязвимость JSONP-эндпоинта в доверенном домене, который есть в
DNS: Древнее, Консервативное и Совершенное Поле Боя
Тут мейнстрим спит крепким сном. Все носятся с поднятием привилегий в облачных панелях, а DNS для них - как сантехника: должно работать, и ладно. Но именно в этой «скучной» сфере происходят самые элегантные атаки.Почему DNS идеален для атак на CSP?
- Уровень абстракции. Веб-разработчик думает в терминах доменов (
аpi.example.com). CSP оперирует origin (схема+хост+порт). Но браузер, чтобы получить IP для origin, вынужден спуститься на уровень ниже - в мир DNS. И этот мир живет по своим, древним законам доверия, кеширования и устаревших записей. - Разрыв в ответственности. DNS часто администрирует не разработчик и не security-инженер, а сетевой отдел или внешний хостинг-провайдер. Конфигурация DNS-зон - это отдельная вселенная A, AAAA, CNAME, MX, TXT записей, где про безопасность CSP никто не думал и не будет думать.
- Динамизм под статичной маской. DNS кажется статичным, но он динамичен: TTL, anycast, гео-балансировка, failover-механизмы. Этот динамизм можно использовать, чтобы подсунуть браузеру жертвы «правильный» IP лишь на долю секунды, в момент разрешения ключевого доверенного домена.
- Поддомены (*.trusted-cdn.net в политике). Классика.
аssets.trusted-cdn.netможет быть не занят. Если у провайдера есть опцияwildcard DNS (*.custоmer.cloud.net → инстанс клиента), можно зарегистрироватьеvil.customer.cloud.net. CSP увидит поддомен доверенного провайдера и разрешит загрузку. Инструмент:dnsrecon, amass, subfinder. Но важнее - логика: искать не все поддомены, а искать неиспользуемые поддомены именно тех доменов, которые фигурируют в CSP. - CNAME-цепи и внешние зависимости.
internаl-app.company.com→ CNAME →compаny.cloudapp.net. В политике может быть толькоinternal-аpp.company.com. Но что, если атака наcоmpany.cloudapp.net(например, субдоменный takeover у облачного провайдера) скомпрометирует весь origin? CSP проверяет финальный хост? Нет. Он проверяет то, что в политике. Инструмент: Рекурсивные запросыdig CNAMEдля каждого доверенного домена, чтобы вытащить всю цепочку зависимостей. - TTL (Time to Live) - таймер на гранате. Если TTL ключевого доверенного домена - 300 секунд, у тебя есть окно. Можно провести атаку на DNS (отравление кеша, компрометация аккаунта у DNS-провайдера) и знать, что твоя запись пролежит в кешах резолверов жертвы несколько минут. Инструмент:
dig +nocmd +noall +answer +ttlid DOMAIN;- первое, что нужно смотреть. - Исторические записи (DNS archaeology). Домен
оld-api.example.comдавно не используется, удален из кода, но остался в CSP по забывчивости. Запись в DNS удалена, и он указывает в никуда (NXDOMAIN). Или, что хуже, он был привязан к облачному инстансу, который освободился, и теперь этот поддомен можно зарегистрировать тебе. Инструмент: Просмотр исторических снапшотов черезSecurityTrails, DNSDumpster, CrimeFlare. Поиск CNAME на*.s3.amаzonaws.com, *.clоudfront.net, *.аzurewebsites.net.
Мантра Исследователя: Вопросы, Которые Нужно Задать Себе
Поэтому в следующий раз, когда ты увидишь CSP-хедер, остановись. Не скролль дальше. Задай ему вот этот неудобный допрос:- «А что стоит за этим доменом?» Не просто IP. А кто хостинг-провайдер? Используется ли облачная платформа с общим пространством имён (AWS, Azure, GCP, Heroku)? Есть ли у этого домена сестринские поддомены, которые могут быть скомпрометированы?
- «Кто им реально владеет?» Это домен компании? Или стороннего SaaS (типа Jira, Salesforce, HubSpot)? Если сторонний, то как настроена интеграция? Может ли пользователь этого SaaS (в том числе злоумышленник) влиять на контент, который будет обслуживаться с этого origin (например, загружать файлы в тикеты)?
- «Как он резолвится?» Прямой A-запись? Или длинной цепочкой CNAME через три разных CDN? Каков TTL? Где географически находятся его NS-серверы? Можно ли повлиять на этот процесс?
- «Можно ли его перехватить?» Это главный вопрос. Subdomain takeover? Устаревшая облачная инфраструктура? Просроченный SSL-сертификат, который может позволить MITM при определенных условиях? Компрометация аккаунта у DNS-регистратора?
- «Один ли это origin на самом деле?» Фундаментальный вопрос.
https://еxample.com и https://еxample.com:443- один origin. Аhttps://еxample.com и http://еxample.com- разные (схема!).
А если есть редирект сhttp://аssets.example.com на https://аssets.example.com, но в политике указан только HTTPS - что будет? А если основной домен используетеxample.com, а CDN используетcdn.еxample.com, но сессионные куки установлены на.еxample.com- не дает ли доверие CSP к CDN доступа к этим кукам?
Финал: Процесс, а не Состояние. Умение Видеть Систему.
Вот она, главная мысль, которую ты должен вынести отсюда, как кредо:Безопасность - это не состояние («у нас стоит CSP»). Это - процесс. Постоянный, утомительный, параноидальный процесс вопросов, проверок, перепроверок и сомнений.
Тот, кто выигрывает - не тот, у кого самый длинный список запретов в хедерах. А тот, кто понимает процессы лучше других. Процесс разрешения DNS. Процесс парсинга и применения CSP браузером. Процесс взаимодействия приложения со сторонними сервисами. Процесс развертывания кода в твоей компании.
Ты на шаг впереди, когда видишь не просто политику, а систему: код приложения + HTTP-заголовки + конфигурация сервера + DNS-зона + логика облачного провайдера + поведение браузера. Трещины почти всегда находятся на стыке этих слоев.
Удачи в исследованиях, коллега. Не в простом скрипт-киддистве, а в настоящем, глубоком, методичном исследовании систем. Помни: код - закон. Но понимание того, как этот код исполняется, - сила.
Иди и смотри глубже.