Content Security Policy (CSP) и Security-заголовки
CSP — критический уровень защиты от XSS и инъекций. Security-заголовки вместе образуют комплексную защиту браузерного слоя.
CSP
MUST
- CSP обязателен для всех приложений с пользовательским контентом или внешними ресурсами
- Использовать
nonceилиsha256-hashдля inline-скриптов вместоunsafe-inline - Ограничение встраивания через
frame-ancestors 'none'илиframe-ancestors 'self' - Внешние скрипты — только с явно разрешённых доменов
SHOULD
- Тестировать CSP в режиме
Content-Security-Policy-Report-Onlyперед включением - Использовать
upgrade-insecure-requestsдля автоматического апгрейда HTTP → HTTPS - Включать
require-trusted-types-for 'script'как дополнительный уровень защиты
FORBIDDEN
unsafe-eval— блокируетeval(),new Function(), строковые аргументы вsetTimeoutunsafe-inlineбезnonceframe-ancestors '*'
Базовая конфигурация
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import crypto from 'crypto';
export function middleware(request: NextRequest) {
const nonce = crypto.randomBytes(16).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
.replace(/\n/g, ' ')
.trim();
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', cspHeader);
response.headers.set('X-Nonce', nonce);
return response;
}
Директивы CSP
| Директива | Рекомендация | Описание |
|---|---|---|
default-src 'self' | MUST | Базовая политика |
script-src 'nonce-{N}' | MUST | Только скрипты с nonce |
object-src 'none' | MUST | Запрет <object>, <embed> |
frame-ancestors 'none' | MUST | Запрет встраивания в iframe |
base-uri 'self' | MUST | Ограничение <base> |
connect-src | MUST | Явный список API endpoints |
form-action 'self' | SHOULD | Ограничение форм |
upgrade-insecure-requests | SHOULD | HTTP → HTTPS |
Примеры конфигураций
// ✅ Строгая конфигурация для новых проектов
const strictCSP = `
default-src 'none';
script-src 'self' 'nonce-{NONCE}' 'strict-dynamic';
style-src 'self' 'nonce-{NONCE}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
// ❌ FORBIDDEN: Небезопасная конфигурация
const unsafeCSP = `
default-src *;
script-src * 'unsafe-inline' 'unsafe-eval';
style-src * 'unsafe-inline';
`;
CSP Report-Only (тестирование)
// ✅ SHOULD: Тестирование перед включением
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy-Report-Only',
value: `default-src 'self'; report-uri /api/csp-report;`,
},
],
},
];
},
};
// API endpoint для сбора отчётов
// app/api/csp-report/route.ts
export async function POST(request: Request) {
const report = await request.json();
console.error('CSP Violation:', {
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
});
return new Response('OK', { status: 200 });
}
Security HTTP-заголовки
MUST
Помимо CSP, обязательны следующие заголовки:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
// Запрет MIME-sniffing
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// Ограничение Referer
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
// Защита от clickjacking (дублирует frame-ancestors для старых браузеров)
{ key: 'X-Frame-Options', value: 'DENY' },
// Ограничение доступа к браузерным API
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
// CSP
{ key: 'Content-Security-Policy', value: cspHeader },
],
},
];
},
};
| Заголовок | Значение | Назначение |
|---|---|---|
X-Content-Type-Options | nosniff | Запрет MIME-sniffing |
Referrer-Policy | strict-origin-when-cross-origin | Ограничение передачи URL в Referer |
X-Frame-Options | DENY или SAMEORIGIN | Защита от clickjacking (legacy) |
Permissions-Policy | camera=(), microphone=() | Ограничение доступа к браузерным API |
Примечание:
X-Frame-Optionsдублируетframe-ancestorsCSP для поддержки старых браузеров.
Subresource Integrity (SRI)
MUST
При загрузке скриптов и стилей с внешних CDN — обязательно использовать атрибут integrity.
SRI гарантирует, что файл не был подменён на CDN. Без SRI компрометация CDN = компрометация всех проектов, использующих этот ресурс.
<!-- ✅ MUST: SRI для внешних ресурсов -->
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"
integrity="sha384-GmbMDGBBNXGq9j+C4k3DPNRiSBH6Pj4CILq0sEGFHUwhVxqp5FPSd7L4YDlJzZg"
crossorigin="anonymous"
></script>
<link
rel="stylesheet"
href="https://cdn.example.com/styles.css"
integrity="sha384-abc123..."
crossorigin="anonymous"
/>
# Генерация SRI-хеша
openssl dgst -sha384 -binary file.js | openssl base64 -A
# Или онлайн: https://www.srihash.org/
FORBIDDEN
Загружать скрипты / стили с внешних CDN без integrity
Ограничение SRI
SRI применим только к статическим (версионированным) ресурсам. Динамически генерируемые ресурсы (например, Google Fonts CSS) не поддерживают SRI, так как содержимое зависит от User-Agent.
Third-Party Scripts
MUST
- Загружать через CSP с явно разрешённых доменов
SHOULD
- При возможности изолировать в
<iframe>с атрибутомsandbox - Проводить аудит third-party scripts при добавлении и при обновлении
<!-- ✅ SHOULD: Изоляция виджета в sandboxed iframe -->
<iframe
src="https://widget.trusted-service.com"
sandbox="allow-scripts allow-same-origin"
title="Third-party Widget"
></iframe>
Примеры конфигурации CSP с внешними сервисами
// ✅ Google Analytics
const gaCSP = `
script-src 'self' 'nonce-{NONCE}' https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' data: https://www.google-analytics.com;
`;
// ✅ Stripe
const stripeCSP = `
script-src 'self' 'nonce-{NONCE}' https://js.stripe.com;
frame-src https://js.stripe.com https://hooks.stripe.com;
connect-src 'self' https://api.stripe.com;
`;