Content Security Policy (CSP)
CSP - это дополнительный уровень безопасности, помогающий обнаружить и смягчить определенные типы атак, включая XSS и code injection.
Правила
MUST
✅ MUST: CSP обязателен для всех приложений с пользовательским контентом
✅ MUST: Использовать nonces или hashes для inline-скриптов вместо unsafe-inline
✅ MUST: Ограничение встраивания во фреймы через frame-ancestors
FORBIDDEN
❌ FORBIDDEN: Использование unsafe-eval
❌ FORBIDDEN: Использование unsafe-inline без nonces
SHOULD
✅ SHOULD: Тестировать CSP в режиме Content-Security-Policy-Report-Only перед внедрением
✅ SHOULD: Использовать upgrade-insecure-requests для автоматического апгрейда HTTP → HTTPS
Базовая конфигурация
// next.config.js
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;
`;
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
];
},
};
Генерация nonce для Next.js
App Router
// app/layout.tsx
import { headers } from 'next/headers';
import crypto from 'crypto';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// ✅ MUST: Генерация уникального nonce для каждого запроса
const nonce = crypto.randomBytes(16).toString('base64');
return (
<html lang="en">
<head>
<meta
httpEquiv="Content-Security-Policy"
content={`
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
`.replace(/\n/g, '')}
/>
</head>
<body>{children}</body>
</html>
);
}
Middleware подход
// 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' data:;
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;
}
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};
Директивы CSP
Таблица директив
| Директива | Описание | Рекомендация |
|---|---|---|
default-src 'self' | Базовая политика для всех ресурсов | MUST |
script-src 'self' 'nonce-{NONCE}' | Разрешены только скрипты с nonce | MUST |
style-src 'self' 'nonce-{NONCE}' | Разрешены только стили с nonce | MUST |
img-src 'self' https: | Изображения с HTTPS | SHOULD |
font-src 'self' data: | Шрифты из своего домена и data: URIs | SHOULD |
connect-src 'self' https://api.example.com | Разрешенные API endpoints | MUST |
object-src 'none' | Запрет <object>, <embed>, <applet> | MUST |
frame-ancestors 'none' | Запрет встраивания в iframe | MUST |
base-uri 'self' | Ограничение <base> элемента | MUST |
form-action 'self' | Ограничение отправки форм | SHOULD |
upgrade-insecure-requests | Автоапгрейд HTTP → HTTPS | SHOULD |