Налаштування CSP (Content Security Policy) для сайту
Content Security Policy — заголовок, який сообщає браузеру, звідки дозволено завантажувати скрипти, стилі, шрифти, зображення та інші ресурси. Грамотно налаштована CSP робить XSS-атаки практично марними: навіть якщо атакуючий внедрить вредоносний скрипт, браузер його заблокує.
Анатомія директив
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com 'nonce-{RANDOM}';
style-src 'self' https://fonts.googleapis.com 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Ключові директиви:
| Директива | Контролює |
|---|---|
script-src |
Звідки завантажуються JS-скрипти |
style-src |
Звідки завантажуються CSS |
img-src |
Джерела зображень |
connect-src |
XHR, fetch, WebSocket |
frame-ancestors |
Хто може вбудовувати сторінку в iframe |
form-action |
Куди відправляються форми |
Nonce-based підхід
'unsafe-inline' для скриптів сводить CSP на нівець. Замість нього використовують nonce — випадкове значення, генероване на сервері для кожного запиту:
// PHP/Laravel
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'self' 'nonce-{$nonce}'");
// У шаблоні
<script nonce="{{ $nonce }}">
// цей скрипт пройде перевірку
</script>
У Next.js через middleware:
// middleware.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export function middleware(request: Request) {
const nonce = crypto.randomBytes(16).toString('base64');
const csp = `script-src 'self' 'nonce-${nonce}'; ...`;
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}
Режим Report-Only
Перед принудовим застосуванням CSP запускають в режимі спостереження:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Браузер відправляє JSON-звіти про порушення, не блокуючи контент. Аналіз звітів за 1–2 тижні показує, які джерела потрібно додати в whitelist.
Приклад звіту:
{
"csp-report": {
"document-uri": "https://example.ua/page",
"violated-directive": "script-src-elem",
"blocked-uri": "https://evil.com/payload.js",
"disposition": "report"
}
}
Складності з SPA та CDN
React/Vue/Angular застосунки часто використовують eval() або динамічну генерацію скриптів через webpack. Це створює конфлікт з CSP:
-
Webpack:
devtool: 'source-map'замістьeval, настройкаTrustedTypes -
Google Analytics / GTM: додати
https://www.google-analytics.comтаhttps://www.googletagmanager.comвscript-srcтаconnect-src -
Inline-стилі з JS-бібліотек: використовувати
style-srcз'unsafe-inline'тільки якщо альтернатив немає, або перейти на CSS-класи
Налаштування в Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; ..." always;
Для динамічних nonce краще керувати заголовком на рівні застосунку, а не Nginx.
Моніторинг порушень
Підключити endpoint для збору CSP-звітів або використовувати сторонні сервіси: Report URI, Sentry (підтримує CSP reporting). Це дозволяє відловити легітимні джерела, забуті в політиці, та відстежити реальні спроби XSS.
Типові помилки
-
'unsafe-inline'та'unsafe-eval'вscript-src— скасовують захист від XSS - Wildcard
*вdefault-src— те ж саме - Відсутність
frame-ancestors— сайт уразливий до Clickjacking - Забути
wss://вconnect-srcпри використанні WebSocket
Строк реалізації
- Аудит поточних джерел ресурсів: 2–4 години
- Настройка Report-Only + збір даних: 1–2 тижні
- Перехід в enforcement-режим з відладкою: 3–5 днів







