Content Security Policy (CSP)

CSP

Что это такое

Content Security Policy — это набор правил, которые сервер посылает вместе со страницей и которые говорят браузеру, что эта страница вправе загружать и выполнять: какие источники могут отдавать скрипты, разрешены ли встроенные скрипты и стили, куда страница может отправлять данные форм и подключаться и можно ли встраивать её во фрейм. Политику исполняет браузер; внедрённый скрипт атакующего не голосует.

Почему это важно

Экранирование вывода — основная защита от внедрения скриптов, и иногда оно даёт сбой: шаблон с лазейкой, санитайзер с пропуском, сторонний виджет, который никто не проверял. CSP — тот слой, который превращает такой сбой из компрометации сессии в заблокированный запрос и сообщение в консоли. Строгая политика ещё и делает сторонний код видимым: каждый новый источник скриптов приходится добавлять осознанно, а это ровно то ревью, которое вам и нужно.

Ключевые идеи

Начните с политики «запретить всё» и откройте нужное

Ограничительное значение по умолчанию — default-src 'none', дальше добавляйте источники, которые страница действительно использует. Заметьте, что 'unsafe-inline' в script-src отключает почти всю защиту — это переключатель, возвращающий политику атакующему.

Content-Security-Policy:
  default-src 'none';
  script-src 'self' 'nonce-<random per response>';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';

Каждая строка отвечает на один вопрос: какой код может выполняться, откуда могут приходить изображения, куда можно отправлять данные и кто может встроить страницу во фрейм.

Используйте nonce вместо разрешения встроенных скриптов

Случайный nonce на каждый ответ в ваших собственных тегах <script> позволяет браузеру отличить ваш встроенный скрипт от внедрённого. Для фиксированного скрипта так же работает хеш. Именно это делает строгую политику возможной на странице, которой действительно нужен встроенный код.

script-src 'self' 'nonce-8f2b...'   -> выполняются только скрипты с этим nonce

Сначала включайте в режиме только отчётов

Отправляйте Content-Security-Policy-Report-Only с эндпоинтом для отчётов, неделю собирайте нарушения и исправьте законные, прежде чем включать принуждение. Принудительно включённая политика, написанная по догадкам, ломает страницу так, что о поломках узнают пользователи.

Держите политику там, где отдаётся страница

Политика, заданная тегом meta, не покрывает всего, что покрывает заголовок, и шаблон легко уходит от неё. Отправляйте её заголовком ответа и генерируйте по странице там, где меняется nonce.

Считайте сторонние источники проверяемым списком

Каждый источник, добавленный в script-src, — это код, который выполняется на странице. Держите список коротким, с датами и владельцем.

Практика

Напишите политику для одной страницы и проверьте её в режиме отчётов

Возьмите настоящую страницу, напишите ограничительную политику, разверните её на неделю в режиме только отчётов и перечислите нарушения.

Готово, когда: вы можете объяснить каждое нарушение — либо законный источник, который надо добавить, либо ошибка, которую надо исправить, — и политика включается принудительно, не ломая страницу.

Уберите один 'unsafe-inline'

Найдите страницу, чья политика содержит 'unsafe-inline', и замените его на nonce или хеш для скриптов, которым он действительно нужен.

Готово, когда: политика работает принудительно без 'unsafe-inline' и ни один скрипт не заблокирован.

Частые ошибки

  • 'unsafe-inline' оставлен на месте — внедрённый скрипт разрешён, и политика ничего не защищает; исправить: используйте nonce или хеш.
  • Принудительное включение без проверки — страница ломается у пользователей; исправить: сначала режим только отчётов и исправление законных нарушений.
  • Политика задана только в теге meta — покрытие неполное и уходит вместе с шаблоном; исправить: отправляйте её заголовком ответа.
  • Источники скриптов через подстановочный знак — код на страницу может отдать любой хост; исправить: называйте источники и проверяйте каждое добавление.
  • Забытый frame-ancestors — страницу можно встроить во фрейм и перехватывать клики; исправить: задайте 'none', если встраивание не является функцией.

Что читать дальше