Content Security Policy (CSP)
CSP — HTTP-заголовок, который указывает браузеру, какие ресурсы можно загружать и откуда. Мощная защита от XSS и data injection атак.
Зачем нужно
Даже если злоумышленник нашёл XSS-уязвимость, CSP не даст выполнить inline-скрипт или загрузить скрипт с чужого домена. Это второй рубеж обороны после санитизации ввода. CSP также предотвращает clickjacking, подмену ресурсов и утечку данных.
Где используется
Все публичные веб-приложения. GitHub, Google, Facebook, Twitter — все используют CSP. Обязательно для приложений, работающих с пользовательскими данными.
Предпосылки
XSS, HTTPS и SSL, HTTP-заголовки
Как работает CSP
1. Сервер отправляет HTTP-заголовок:
Content-Security-Policy: script-src 'self' https://cdn.example.com
2. Браузер разрешает ТОЛЬКО:
- Скрипты с того же домена ('self')
- Скрипты с cdn.example.com
3. Всё остальное БЛОКИРУЕТСЯ:
- Inline-скрипты (<script>alert('XSS')</script>)
- Скрипты с evil.com
- eval, setTimeout('string')
Установка CSP
Через HTTP-заголовок (рекомендуется)
// Express.js
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'"
);
next;
});
// Через Helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.example.com'],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.example.com'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
frameSrc: ["'none'"],
},
}));
Через meta-тег (ограниченно)
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
Ограничения meta: нельзя использовать frame-ancestors, report-uri, sandbox.
Директивы CSP
Основные директивы
| Директива | Контролирует | Пример |
|---|---|---|
default-src |
Всё по умолчанию | 'self' |
script-src |
JavaScript | 'self' https://cdn.com |
style-src |
CSS | 'self' 'unsafe-inline' |
img-src |
Изображения | 'self' data: https: |
connect-src |
fetch, XHR, WebSocket | 'self' https://api.com |
font-src |
Шрифты | 'self' https://fonts.gstatic.com |
frame-src |
iframe | 'none' |
media-src |
Audio, Video | 'self' |
object-src |
Flash, Java applets | 'none' |
base-uri |
Тег <base> |
'self' |
form-action |
Куда отправлять формы | 'self' |
frame-ancestors |
Кто может вставить в iframe | 'none' |
Значения источников
| Значение | Описание |
|---|---|
'self' |
Тот же origin (протокол + домен + порт) |
'none' |
Ничего не разрешено |
'unsafe-inline' |
Inline-скрипты/стили (старайся избегать!) |
'unsafe-eval' |
eval и подобные (избегай!) |
https: |
Любой HTTPS-источник |
data: |
Data URI (data:image/png;base64,...) |
https://cdn.com |
Конкретный домен |
*.example.com |
Все поддомены |
Nonce — безопасные inline-скрипты
Вместо 'unsafe-inline' используй одноразовые nonce:
// Сервер генерирует уникальный nonce на каждый запрос
const crypto = require('crypto');
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'`
);
next;
});
<!-- Только скрипты с правильным nonce выполнятся -->
<script nonce="rAnd0mN0nCe">
console.log('Разрешено!');
</script>
<!-- Без nonce — заблокировано -->
<script>
console.log('Заблокировано CSP!');
</script>
Hash — разрешение конкретных скриптов
Content-Security-Policy: script-src 'sha256-xyz123...'
<!-- Только скрипт с этим точным содержимым выполнится -->
<script>console.log('hello');</script>
# Вычислить hash скрипта
echo -n "console.log('hello');" | openssl dgst -sha256 -binary | openssl base64
Примеры CSP для разных сценариев
Минимальный (строгий)
Content-Security-Policy:
default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self';
connect-src 'self';
font-src 'self';
base-uri 'self';
form-action 'self';
frame-ancestors 'none'
SPA с CDN и API
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.myapp.com wss://ws.myapp.com;
frame-ancestors 'none';
base-uri 'self'
С Google Analytics и reCAPTCHA
Content-Security-Policy:
default-src 'self';
script-src 'self' https://www.googletagmanager.com https://www.google.com https://www.gstatic.com;
style-src 'self' 'unsafe-inline';
img-src 'self' https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com;
frame-src https://www.google.com
Report-Only — тестирование CSP
// Сначала только ОТЧЁТ, без блокировки
res.setHeader(
'Content-Security-Policy-Report-Only',
"default-src 'self'; report-uri /csp-report"
);
// Endpoint для получения отчётов
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP violation:', req.body);
res.status(204).end();
});
Отчёт приходит в формате:
{
"csp-report": {
"document-uri": "https://myapp.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://evil.com/script.js",
"original-policy": "default-src 'self'; script-src 'self'"
}
}
Частые ошибки
1. 'unsafe-inline' для скриптов
// ПЛОХО: обнуляет защиту от XSS
script-src 'self' 'unsafe-inline'
// ХОРОШО: используй nonce или hash
script-src 'self' 'nonce-rAnd0m'
2. Слишком мягкий default-src
// ПЛОХО: разрешает всё
default-src *
// ХОРОШО: запрещает по умолчанию
default-src 'none'
3. Забыли connect-src для API
// Fetch/XHR к API заблокирован!
// Нужно явно разрешить:
connect-src 'self' https://api.example.com
4. Не тестировали через Report-Only
// Всегда начинай с Report-Only, чтобы не сломать сайт
Content-Security-Policy-Report-Only: ...
// Потом, когда ошибок нет, переключай на Content-Security-Policy
Практика
- Добавь CSP-заголовок в Express-приложение через Helmet
- Настрой
script-srcс nonce для inline-скриптов - Используй
Content-Security-Policy-Report-Onlyи проверь, что все ресурсы загружаются - Попробуй выполнить inline-скрипт при включённом CSP — убедись, что он заблокирован
- Настрой CSP для сайта с Google Fonts и внешним CDN