UptimeChecker

Content-Security-Policy: настройка заголовка

Обложка: 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' в ней будет проигнорирован браузером — так работают переходные периоды.

Директивы CSP для источников страницы

Основные директивы и их назначение

Директив в спецификации несколько десятков, но в повседневной настройке хватает стабильного набора. Сведём их в таблицу.

Директива Управляет Типичное значение
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': формально заголовок есть, защиты нет. Такая конфигурация только создаёт ложное ощущение безопасности.

Ошибка CSP в консоли браузера

Режим отчётов: 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 используются посетителями, не сломав ничего у живых пользователей.

Рабочий процесс миграции выглядит так:

  1. Замер. Ставите Content-Security-Policy-Report-Only с нужной политикой и эндпоинтом report-uri. Собираете данные 1–2 недели.
  2. Разбор. Читаете отчёты: какие директивы нарушаются и какими ресурсами. Часто находятся забытые виджеты и старые библиотеки, о которых никто не помнил.
  3. Доработка. Убираете лишние источники из шаблонов или добавляете нужные в allowlist. Для inline-скриптов внедряете nonce.
  4. Включение. Меняете заголовок с Content-Security-Policy-Report-Only на Content-Security-Policy. report-uri имеет смысл оставить — отчёты пригодятся и после запуска.
  5. Ужесточение. Постепенно убираете '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

Проверка 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-сертификат и заголовки безопасности, чтобы убедиться, что конфигурация на месте не только в день настройки, но и после каждого релиза.