Content-Security-Policy: настройка заголовка
Что такое CSP и от чего она защищает
Content-Security-Policy (CSP) — HTTP-заголовок, который задаёт браузеру правила: откуда странице разрешено загружать скрипты, стили, картинки, шрифты и фреймы. Политика сообщает не «что нельзя», а «что можно»: всё, что не разрешено явно, браузер блокирует и пишет об этом в консоль.
Основной сценарий, ради которого CSP и придуман, — XSS (cross-site scripting). Если злоумышленник смог внедрить на страницу свой скрипт (через поле комментария, параметр URL, скомпрометированную библиотеку), без CSP этот скрипт выполнится в контексте вашего сайта: прочитает cookie, токены из localStorage, отправит запросы от имени пользователя. CSP позволяет сказать браузеру: «исполняй только те скрипты, что лежат на моём домене и имеют конкретный nonce» — и внедрённый код просто не запустится.
Что ещё закрывает CSP:
- Утечки данных через подставные адреса. Директивы
connect-srcиimg-srcне дают скрипту отправить данные на чужой домен. - Кликджекинг. Директива
frame-ancestorsзапрещает встраивать ваш сайт в чужой iframe. - Подмену базового адреса.
base-uriблокирует подмену тега<base>, через которую относительные ссылки уводят на чужой хост. - Загрузку плагинов и смешанного контента. Директивы вроде
object-srcи общий принцип запрета небезопасных схем (http::) отключают устаревшие векторы атак.
Важно понимать границы: CSP — не замена санитайзингу пользовательского ввода и не замена HSTS. HSTS отвечает за транспорт (чтобы соединение всегда было HTTPS), CSP — за то, что происходит на уже загруженной странице. Это два независимых слоя, и настраивать их логично вместе. Подробнее о транспортном слое — в статье про HSTS.
Синтаксис заголовка Content-Security-Policy
Заголовок состоит из директив, разделённых точкой с запятой. Каждая директива задаёт тип ресурса и список источников:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' fonts.example.com; img-src 'self' data: https:; font-src fonts.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
Разберём значение по частям:
default-src— резервное правило для всех типов ресурсов, у которых нет своей директивы. Заданныйdefault-srcизбавляет от необходимости перечислять каждый тип.'self'— текущий источник (схема + домен + порт). Одинарные кавычки обязательны, это часть синтаксиса.'nonce-abc123'— разрешает конкретный inline-скрипт с атрибутомnonce="abc123". Значение nonce должно быть криптостойким и уникальным на каждый ответ.data:— разрешает ресурсы со схемойdata:. Используется для встроенных картинок.https:— разрешает HTTPS с любого хоста. Такой источник ослабляет политику, применять его стоит осознанно.
Общее правило CSP: директивы, которые не указаны, берутся из default-src. Если и default-src не задан, тип ресурсов не ограничен. Именно поэтому default-src 'self' — минимальная осмысленная база.
Список источников внутри директивы — это allowlist. Совпадение идёт по схеме, хосту и порту. Несколько практических правил записи:
| Запись источника | Что разрешает |
|---|---|
'self' |
Только текущий origin |
'none' |
Ничего (директива полностью запрещена) |
example.com |
Любую схему на этом хосте |
https://example.com |
Только HTTPS на этом хосте |
*.example.com |
Все поддомены example.com |
https: |
Любой хост по HTTPS |
data: |
Data-URI |
'unsafe-inline' |
Все inline-скрипты и обработчики (небезопасно) |
'unsafe-eval' |
eval(), new Function() (небезопасно) |
Директивы 'unsafe-inline' и 'unsafe-eval' фактически отключают основную защиту от XSS и нужны только как временная мера при миграции. Если директива содержит 'nonce-...' или 'hash-...', то 'unsafe-inline' в ней будет проигнорирован браузером — так работают переходные периоды.

Основные директивы и их назначение
Директив в спецификации несколько десятков, но в повседневной настройке хватает стабильного набора. Сведём их в таблицу.
| Директива | Управляет | Типичное значение |
|---|---|---|
default-src |
Резервное правило для всех типов | 'self' |
script-src |
Загрузка и исполнение скриптов | 'self' 'nonce-...' |
script-src-elem |
Скрипты в тегах <script> |
'self' 'nonce-...' |
style-src |
Таблицы стилей и <style> |
'self' 'unsafe-inline' (до миграции) |
img-src |
Картинки, иконки, canvas | 'self' data: https: |
font-src |
Шрифты | 'self' fonts.gstatic.com |
connect-src |
XHR, fetch, WebSocket, EventSource | 'self' api.example.com |
frame-src |
Встраиваемые фреймы | 'self' |
frame-ancestors |
Кто может встроить вашу страницу | 'none' или домены |
form-action |
Куда отправляются формы | 'self' |
base-uri |
Значение тега <base> |
'self' |
media-src |
Видео и аудио | 'self' или CDN |
object-src |
Flash, Java, прочие плагины | 'none' |
worker-src |
Web Workers и Service Workers | 'self' |
report-uri |
Куда отправлять отчёты о нарушениях | URL эндпоинта |
upgrade-insecure-requests |
Переписывать HTTP-ресурсы на HTTPS | без значения |
Несколько директив заслуживают отдельного внимания.
script-src — главная директива. Именно она блокирует XSS. Самый надёжный вариант — nonce или hash: сервер при каждом ответе генерирует случайное значение и подставляет его в тег <script nonce="...">, а CSP разрешает только скрипты с этим значением. Так любой внедрённый скрипт (без nonce) не выполнится, даже если он попал в HTML.
frame-ancestors заменила устаревший заголовок X-Frame-Options и гибче него: позволяет перечислить домены, которым разрешено встраивать вашу страницу. Для обычного сайта значение 'none'.
upgrade-insecure-requests — директива без списка источников. Она говорит браузеру переписывать http://-ссылки на ресурсы в https://, что избавляет от части проблем со смешанным контентом при миграции.
report-uri (и более новый report-to) — куда уходит JSON с описанием заблокированного ресурса. Это основа режима отчётов, про который ниже.
Примеры политик: от базовой до строгой
1. Базовая политика для статического сайта. Минимум, который уже даёт защиту:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Здесь всё загружается только с вашего домена, плагины отключены, встраивание сайта запрещено.
2. Сайт с внешними сервисами (шрифты, аналитика, CDN картинок).
Content-Security-Policy: default-src 'self'; script-src 'self' https://static.example.com; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://cdn.example.com; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
Обратите внимание: каждый сторонний сервис приходится вносить в явный список. Это не неудобство, а смысл CSP — вы осознанно фиксируете, кому доверяете.
3. Строгая политика с nonce для динамической страницы. Сервер подставляет уникальный nonce в заголовок и в теги скриптов:
Пример string-значения заголовка ответа:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-8f3a1c9e' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
<script nonce="8f3a1c9e" src="/app.js"></script>
Директива 'strict-dynamic' доверяет скриптам, которые загрузил уже доверенный скрипт — это упрощает работу с современными сборщиками, которые сами подтягивают чанки.
Чего делать не стоит. Собирать политику из одних * и 'unsafe-inline': формально заголовок есть, защиты нет. Такая конфигурация только создаёт ложное ощущение безопасности.

Режим отчётов: Content-Security-Policy-Report-Only
Внедрять строгую CSP сразу опасно: одна забытая директива — и на проде перестанут грузиться стили или отвалится аналитика. Для безопасного перехода есть отдельный заголовок:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri https://example.com/csp-report
С ним браузер не блокирует нарушения, а только фиксирует их и отправляет отчёты на указанный адрес. Это позволяет месяц наблюдать реальную картину: какие скрипты, шрифты и API используются посетителями, не сломав ничего у живых пользователей.
Рабочий процесс миграции выглядит так:
- Замер. Ставите
Content-Security-Policy-Report-Onlyс нужной политикой и эндпоинтомreport-uri. Собираете данные 1–2 недели. - Разбор. Читаете отчёты: какие директивы нарушаются и какими ресурсами. Часто находятся забытые виджеты и старые библиотеки, о которых никто не помнил.
- Доработка. Убираете лишние источники из шаблонов или добавляете нужные в allowlist. Для inline-скриптов внедряете nonce.
- Включение. Меняете заголовок с
Content-Security-Policy-Report-OnlyнаContent-Security-Policy.report-uriимеет смысл оставить — отчёты пригодятся и после запуска. - Ужесточение. Постепенно убираете
'unsafe-inline'и широкие источники, итеративно, а не одним коммитом.
Формат отчёта — JSON, отправляемый POST-запросом. Ключевые поля:
{
"csp-report": {
"document-uri": "https://example.com/page",
"blocked-uri": "https://ads.example.net/pixel.js",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'",
"disposition": "report"
}
}
document-uri— страница, на которой произошло нарушение.blocked-uri— ресурс, который попытался загрузиться.violated-directive— какая директива сработала.disposition—reportдля Report-Only,enforceпри блокировке.original-policy— полный текст политики на момент срабатывания.
Важная деталь: Content-Security-Policy-Report-Only можно отправлять одновременно с обычной Content-Security-Policy. Так вы включаете строгую политику для ресурсов, которые уже проверили, а вторую (более строгую, тестируемую) — только в режиме отчётов, чтобы протестировать следующую итерацию без риска.

Проверка CSP: curl, браузер, отчёты
Начать стоит с того, что отдаёт сервер:
curl -sI https://example.com | grep -i content-security-policy
Возможные результаты и их смысл:
content-security-policy: default-src 'self'; ...— политика включена и применяется (enforce).content-security-policy-report-only: ...— включён только режим отчётов, блокировки нет.- Пустой вывод — заголовка нет, страница не защищена CSP.
- Политика из одних
'unsafe-inline'и*— формально есть, защиты нет.
Проверить, что заголовок отдаётся и на API-ответах, а не только на HTML:
curl -sI https://example.com/api/v1/status | grep -i content-security-policy
CSP относится к документу, поэтому для JSON-ответов она часто не важна, но если по этому хосту отдаются страницы ошибок или предпросмотры, заголовок нужен и там.
В браузере CSP удобно проверять двумя способами. Во вкладке Network → Response Headers виден сам заголовок — по нему понятно, какая политика пришла для конкретного документа. А в консоли появляются ошибки вида:
Refused to load the script 'https://third-party.example/x.js' because it violates the following Content Security Policy directive: "script-src 'self'".
Эта строка — самый быстрый способ понять, какой именно ресурс режет ваша политика. По ней сразу видно директиву и заблокированный URI.
Если ресурсов много и ошибки сыпятся пачками, смотреть консоль вручную утомительно — именно для этого и существует режим отчётов. Собрав отчёты на центральном эндпоинте, вы увидите полную картину в разрезе страниц и типов ресурсов.
Проверить, не отдаётся ли заголовок в ответе по HTTP (где он не нужен и может вводить в заблуждение), можно командой:
curl -sI http://example.com | grep -i content-security-policy
В корректной конфигурации здесь ожидается редирект на HTTPS. Общий обзор подключённых защитных заголовков на вашем сайте даёт проверка security-заголовков — она показывает, какие из них настроены, а какие отсутствуют.
Настройка CSP: nginx, Apache, шаблоны
nginx — заголовок добавляется в блок server. Для динамического nonce нужен механизм подстановки (например, через переменную, формируемую приложением):
server {
listen 443 ssl http2;
server_name example.com;
# статическая политика
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
# режим отчётов вместо жёсткой блокировки
# add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https:; report-uri /csp-report" always;
}
Параметр always здесь так же важен, как и для HSTS: без него заголовок не попадёт в ответы с кодами ошибок, а страницы ошибок — частый вектор атак.
Apache (mod_headers):
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
</IfModule>
Для фреймворков с шаблонизатором (Laravel, Django, Rails, Express) CSP удобнее задавать на уровне middleware или шаблона: там вы можете генерировать nonce прямо при рендере страницы и передавать его и в заголовок, и в теги <script>. Так политика остаётся строгой без списков внешних хостов.
Один частый подводный камень: заголовок, заданный в нескольких местах (в CDN и в веб-сервере), приводит к тому, что браузер видит одну из политик или обе. Если политики объединяются — итоговая становится строже (пересечение разрешений). Если это неожиданно ломает ресурсы, проверьте, не дублируется ли заголовок на разных уровнях.
Таблица: симптом → причина → решение
| Симптом | Причина | Решение |
|---|---|---|
| Не грузятся стили или скрипты после включения | Ресурс с внешнего хоста не внесён в style-src/script-src |
Добавить хост в нужную директиву или задать nonce |
| Нарушений нет, но и защиты нет | В директивах * или 'unsafe-inline' |
Убрать широкие источники, перейти на nonce/hash |
| Inline-скрипты не работают | Нет nonce на теге <script> |
Генерировать nonce при рендере и подставлять в тег |
| Сайт нельзя встроить в iframe партнёра | frame-ancestors 'none' слишком строгий |
Перечислить разрешённые домены вместо 'none' |
| Сторонний виджет шлёт запросы и падает | Не задан connect-src |
Добавить домен API в connect-src |
| Всё ломается на проде, откат нужен быстро | Политику включили сразу в enforce-режиме | Переключить на Content-Security-Policy-Report-Only, собрать отчёты, включить снова |
| Отчёты не приходят | report-uri некорректен или эндпоинт недоступен |
Проверить URL, доступность и логи приёма |
| Заголовок не виден на страницах ошибок | add_header без always |
Добавить always |
Короткий итог
CSP — это инструкция браузеру, что можно загружать и исполнять на странице. Настраивается она в два уровня: сначала базовая политика с default-src 'self' и object-src 'none', затем ужесточение script-src через nonce. Режим Content-Security-Policy-Report-Only позволяет пройти этот путь без риска для работающего сайта: он показывает реальные нарушения, ничего не блокируя.
Помните о связке с транспортным слоем: CSP защищает контент документа, но не гарантирует, что соединение вообще было HTTPS. Обе части настраиваются вместе, и лучше держать их под регулярным контролем — начните с проверки доступности сайта, затем проверьте SSL-сертификат и заголовки безопасности, чтобы убедиться, что конфигурация на месте не только в день настройки, но и после каждого релиза.