Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Content Security Policy: настройка CSP без поломок

Content Security Policy на практике: защита от XSS без сломанного сайта

Как внедрить CSP: директивы default-src/script-src, nonce и hash для инлайна, report-only режим, типовые поломки и пошаговое ужесточение политики.

Прямой ответ

Content Security Policy — HTTP-заголовок, которым сайт объявляет браузеру «откуда можно грузить и исполнять» (скрипты/стили/картинки/соединения); главный эффект — сокращение ущерба XSS: даже если инъекция прошла (вывод данных без экранирования — OWASP), inline-скрипт атакующего не исполнится (политика запрещает), exfiltration на чужой домен заблокирован (connect-src) — CSP не заменяет экранирование/валидацию (инъекции — первый рубеж), но делает одиночную XSS-дыру не катастрофой. Рабочая база: Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' — стартовый минимум (всё со своего origin, плагины/объекты запрещены, базовый URI закреплён, кликджекинг-фрейминг закрыт); далее по ветвям: style-src (инлайновые стили — частая потребность фреймворков — 'unsafe-inline' или nonce/hash), img-src/font-src (свои + CDN-домены — CDN), connect-src (XHR/fetch/WebSocket-направления — API, аналитика, Sentry), form-action (формы не уходят на чужие хосты — фишинг-вектор закрыт). Главное препятствие — инлайновые скрипты/стили (onclick=, <script> в теле — legacy и шаблоны ими полны): правильный путь — вынос в файлы + nonce (script-src 'nonce-{random}' — сервер генерирует на каждый ответ, тег с тем же nonce разрешён — стандарт для серверного рендеринга; hash — для статичных инлайнов) против «'unsafe-inline' и забыть» (обнуляет защиту скриптов — CSP декоративна). Внедрение без поломок — поэтапно: Content-Security-Policy-Report-Only (политика не блокирует, а репортит нарушения на report-uri — недели телеметрии показывают, что сломается) → фикс источников/директив → боевой заголовок → мониторинг отчётов (новые нарушения = регрессии или атака — CSP как сенсор).

Директивы: что ограничивать и как

Структура политики — список директив «ресурс-класс: источники»: default-src — fallback для всех (ставить 'self' — непокрытое явно не разрешено по умолчанию), script-src — главное (исполняемое: 'self' + nonce/hash; сторонние скрипты — явные домены, не wildcard https: — «любые https-скрипты» = разрешить XSS-загрузчику жить); style-src (стили; фреймворки с runtime-стилями (styled-подходы) требуют 'unsafe-inline' для стилей — компромисс приемлемее, чем для скриптов — стиль-инъекция не исполняет код напрямую, но CSS-утечки (attribute selectors exfiltration) реальны — ужесточать при возможности), img-src 'self' data: (data: для мелких inline-картинок, CDN-домены явно), font-src, connect-src 'self' wss://...(fetch/XHR/WebSocket/[SSE](./http-sse-server-sent-events-praktika.md) — все сетевые направления приложения — недоучтённый connect-src ломает аналитику/Sentry молча в проде — report-phase ловит),frame-ancestors 'none'(кто может встраивать сайт во фрейм — кликджекинг-защита — замена устаревшему X-Frame-Options; 'self' — свои страницы встраивать можно),base-uri 'self'(инъекция <base> перехватывает относительные URL — редкий, но дешёвый вектор),form-action 'self'(перехват отправок форм),object-src 'none'/upgrade-insecure-requests (Flash-наследие мертво; HTTP→HTTPS автоапгрейд ссылок). Значения-источники: 'self' (свой origin — схема+хост+порт), явные домены (cdn.example.com — без схем-звёзд по возможности), nonce-'случайное' (одноразовый токен ответа), 'sha256-...' (хеш инлайн-блока — стабилен, но правка блока = новый хеш), 'unsafe-inline'/'unsafe-eval' (капитуляции — eval нужен некоторым библиотекам (шаблонизаторы в рантайме) — лечится переходом на прекомпиляцию, не политикой — [сборка](./vite-sborka-frontenda-skorost-i-pamyat.md) — компилируемые шаблоны eval не требуют); wildcard'ы (, https:, .example.com`) — минимизировать (широкий wildcard = дыра в политике — субдомен-компрометация или опечатка-домен эксплуатируются).

Nonce, hash и серверная реализация

Nonce-механика (серверный рендеринг — Django/FastAPI+Jinja): на каждый ответ — крипто-случайный nonce (secrets.token_urlsafe(16) — не переиспользовать между запросами/пользователями — иначе обходится), в заголовок (script-src 'self' 'nonce-abc123') и в разрешённые теги (<script nonce="abc123">) — инлайн атакующего без nonce мёртв даже при полной XSS; реализация — middleware (FastAPI-зависимости): генерация nonce → контекст шаблона → заголовок — 30 строк; кеш-нюанс критичен: ответ с nonce не кешируется шаренно (персонализированный заголовок — кеш-дисциплина: private/no-store на HTML — иначе чужой nonce в кешированном HTML блокирует все скрипты — «сайт сломался у части пользователей» — классика CSP+CDN; CDN-вариант — nonce-переписывание edge-функциями — сложно, обычно HTML просто не кешируют). Hash-альтернатива — для статичных инлайнов (небольшой скрипт инициализации — sha256-хеш в политику — без server-side динамики; несколько инлайнов — несколько hash-значений; правка кода — пересчёт хеша — CI-скрипт генерирует — автоматизировать или вынести в файл — вынос проще). SPA-специфика: внешний бандл 'self' (nonce не нужен — файлы, не инлайн), но index.html с inline-бутстрапом — nonce/hash на него; стороннее (аналитика/виджеты/платежные iframe) — точечные директивы (script-src на их скрипты, frame-src на их iframe, connect-src на их эндпоинты — аудит стороннего — половина работы над CSP — «кто всё это подключил» — отдельное открытие — supply chain в клиентском варианте).

Практический сценарий: CSP на существующий сайт за три недели

Исходник: каталог+личный кабинет (серверный рендер + немного vanilla-JS), XSS-находка на стейдже (отражение поиска без экранирования — закрыта фиксом — но «сколько ещё» неизвестно; владелец просит защиту слоем). Неделя 1 — аудит и Report-Only: политика-черновик (default-src 'self'; script-src 'self' 'unsafe-inline' временно; object-src 'none'; base-uri 'self'; frame-ancestors 'self') в режиме Content-Security-Policy-Report-Only + report-to на сборщик (Sentry принимает CSP-отчёты — error-tracking — нуль своей инфраструктуры); телеметрия за неделю: 340 нарушений — инлайн-скрипты в 14 шаблонах, onclick-атрибуты в легаси-таблицах, стили в style= атрибутах (сотни), три сторонних домена (аналитика, карты, платёжка) не учтены в черновике. Неделя 2 — исправления: инлайн-скрипты вынесены в /static/js/модули (сборка — бандлер собрал разрозненные скрипты заодно), onclick → addEventListener (механическая замена по шаблонам — день), сторонние домены в script-src/frame-src/connect-src явно, style-src пока с 'unsafe-inline' (style-атрибуты от отчёт-генератора — миграция на классы отложена — компромисс записан); nonce внедрён middleware'ом на два оставшихся инлайн-блока (конфиг-инициализация). Неделя 3 — ужесточение и бой: 'unsafe-inline' для скриптов убран (отчёты чистые неделю), боевой заголовок включён (Report-Only остался параллельно на более строгую будущую политику — «следующий шаг» в том же режиме), мониторинг: CSP-отчёты в Sentry-алерт (алерты — всплеск нарушений = регресс деплоя или попытка атаки); проверка защиты: учебная XSS (<script>alert(1)</script> в поле отзыва на стейдже — вставка отрендерилась текстом (экранирование) и даже без него — не исполнилась (CSP — второй слой сработал — защита в глубину — OWASP-принцип); отчёт ушёл в Sentry — «атака» видна за секунды. Итог: XSS-радиус сжат (inline-исполнение мертво, exfiltration заблокирован connect-src), сторонний JS взят под учёт (3 домена под контролем вместо «всё, что подключили за 5 лет»), регресс-сенсор работает; стоимость — три недели фоновой работы, ни одного сломанного пользовательского сценария (report-phase окупилась полностью — боевое включение прошло без единого инцидента).

FAQ

CSP заменяет ли экранирование вывода и другие защиты от XSS? Нет — CSP второй слой (defense in depth): экранирование/валидация/фреймворк-автозащита — устраняют инъекции; CSP — ограничивает ущерб прошедшей инъекции (inline не исполняется, чужие домены закрыты, отчёт приходит); «поставили CSP — XSS закрыт» — опасное заблуждение (обходы существуют: разрешённые домены с открытыми редиректами/JSONP-эндпоинтами, nonce-переиспользование, 'unsafe-inline'-остатки); полный контур: экранирование + CSP + httpOnly-куки (украденные XSS-скриптом куки не читаются — аутентификация) + мониторинг отчётов.

Что делать, если библиотека требует 'unsafe-inline' или 'unsafe-eval'? По типам: eval-требующие шаблонизаторы рантайма — миграция на прекомпиляцию (большинство умеет — build-шаг вместо runtime-eval); style-inline от UI-библиотек — 'unsafe-inline' только в style-src (не в script-src — стили-инъекции на порядок менее опасны — приемлемый компромисс, фиксируется решением — ADR); nonce-несовместимые старые сниппеты — hash или вынос в файл; «библиотека без вариантов» — изоляция (iframe с отдельной строгой политикой — sandbox-атрибут — чужой код в карантине); «включить unsafe везде, чтобы заработало» — CSP становится декорацией — лучше честный минимум без script-поблажек, чем полная политика с unsafe-inline в скриптах.

Как CSP дружит с CDN и кешированием? HTML с nonce — персонализирован (каждому свой) — шаренный кеш неприменим (no-cache/no-store на HTML — ассеты с хешами кешируются вечно — схема не теряет производительность); статичная политика (без nonce — только hash/'self') — кешируется нормально; CDN-доставка стороннего — домены в директивы + SRI (Subresource Integrity — hash-проверка скачанного скрипта — CDN-компрометация не подменит код — integrity= атрибут — дополнение к CSP для сторонних скриптов); edge-функции провайдеров умеют nonce-инъекцию (Cloudflare-примеры) — сложность оправдана только при кеше HTML на edge — большинству достаточно «HTML не кешируем».

Нужен ли CSP для API-only сервиса (без страниц)? Минимально полезен: default-src 'none'; frame-ancestors 'none' на API-ответах (защита от «API-ответ отрендерили как страницу»-сценариев и кликджекинга ошибок-страниц); основная CSP-работа — у фронтенда, который API потребляет; но заголовки-гигиена (CSP, X-Content-Type-Options: nosniff, Referrer-Policy — безопасность-набор) ставятся на всех ответах middleware'ом за час (nginx или приложение — один слой выбирается командой) — «API без заголовков» в security-аудитах светится как finding с нулевой стоимостью исправления.

Источники: спецификация CSP3 (W3C), MDN CSP, content-security-policy.com — ссылки в шапке.