Content Security Policy: почему этот заголовок спасает от XSS, но его игнорируют 80% сайтов
Что такое Content Security Policy
CSP — это HTTP-заголовок, который говорит браузеру: «Загружай скрипты, стили и картинки только с этих адресов». Всё остальное блокируется на корню. XSS-атака? Инжект стороннего скрипта? Без явного разрешения в CSP это просто не сработает.
По данным Google, XSS остаётся в топ-3 уязвимостей веб-приложений. При этом CSP существует с 2012 года. Почему его до сих пор мало кто использует? Страх сломать продакшен. Я тоже его испытывал, пока не нашёл правильный подход.
Базовая настройка через nginx
Я работаю в основном с nginx, поэтому добавляю заголовок прямо в конфиг. Вот конфигурация, с которой начинаю на любом новом проекте:
add_header Content-Security-Policy «default-src ’self’; script-src ’self’; style-src ’self’; img-src ’self’ data:; font-src ’self’; frame-ancestors ’none’; base-uri ’self’;» always;
Это строгий вариант: всё грузится только с текущего домена, фреймы запрещены, инлайн-скрипты тоже. Для простого сайта без сторонних виджетов — идеально.
Но реальность сложнее. Практически любой современный сайт использует Google Analytics, Яндекс.Метрику, Google Fonts, виджеты обратного звонка, карты. Тогда белый список растёт. Вот пример для типичного корпоративного сайта на Bitrix:
add_header Content-Security-Policy «default-src ’self’; script-src ’self’ https://mc.yandex.ru https://www.google-analytics.com https://ssl.google-analytics.com; style-src ’self’ ’unsafe-inline’ https://fonts.googleapis.com; img-src ’self’ data: https://mc.yandex.ru https://www.google-analytics.com; font-src ’self’ https://fonts.gstatic.com data:; frame-src https://www.youtube.com; frame-ancestors ’none’; report-uri /csp-report;» always;
Главный лайфхак: Report-Only
Вот что я делаю всегда, когда внедряю CSP на работающий сайт. Сначала включаю заголовок Content-Security-Policy-Report-Only. Он работает точно так же, но не блокирует ресурсы — только отправляет отчёты о нарушениях.
Схема простая: две недели в режиме Report-Only, собираем логи, понимаем, что нужно добавить в белый список, и только потом переключаемся на боевой CSP. Так я не ломаю сайт и не срываю аналитику.
Для приёма отчётов нужен простой скрипт на бэкенде. Я использую минимальный PHP-обработчик, который пишет JSON-логи в файл. После недели сбора обычно понятно, какие домены нужно добавить.
WordPress и Bitrix: особенности
WordPress — отдельная боль. Огромное количество плагинов добавляет инлайн-скрипты прямо в HTML, часто без настроек. Редактор Gutenberg сам требует разрешений, которые делают CSP мягче. На WordPress я обычно иду на компромисс: разрешаю unsafe-inline для стилей, но держу строгий список для скриптов.
На Bitrix ситуация лучше, но есть нюансы. Компонент карт, виджет онлайн-консультанта из Bitrix24, модуль оплаты — всё это тянет внешние ресурсы. На одном B2B-портале я потратил три дня на составление корректного CSP. Особенно пришлось повозиться с CloudPayments — их скрипты грузятся с нескольких CDN-доменов, которые ещё и меняются.
Типичные ошибки при внедрении
Разрешают всё через звёздочку. Встречал конфиги вида script-src *. Это бессмысленно — вы разрешаете скрипты откуда угодно.
Забывают про data: URI. Некоторые библиотеки используют их для шрифтов или картинок. Если не добавить data: в font-src — сломается вёрстка.
Не тестируют в разных браузерах. Chrome и Firefox реализуют CSP немного по-разному. Safari вообще отдельная история.
Паникуют из-за аналитики. Маркетологи боятся, что Метрика перестанет работать. Решается добавлением двух доменов в белый список — это 10 минут работы.
Связка с другими заголовками
CSP работает лучше в команде. Я всегда настраиваю его вместе с HSTS, X-Frame-Options, X-Content-Type-Options и Referrer-Policy. Проверить всё это можно на securityheaders.com — цель минимум A. На моих проектах обычно A или A+.
Вывод
CSP — не замена SSL и не замена firewall. Это отдельный слой защиты, который стоит практически ничего, но экономит миллионы при инциденте. Настроить можно за вечер. Не настроить — значит оставить дверь открытой.