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

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}'Разрешены только скрипты с nonceMUST
style-src 'self' 'nonce-{NONCE}'Разрешены только стили с nonceMUST
img-src 'self' https:Изображения с HTTPSSHOULD
font-src 'self' data:Шрифты из своего домена и data: URIsSHOULD
connect-src 'self' https://api.example.comРазрешенные API endpointsMUST
object-src 'none'Запрет <object>, <embed>, <applet>MUST
frame-ancestors 'none'Запрет встраивания в iframeMUST
base-uri 'self'Ограничение <base> элементаMUST
form-action 'self'Ограничение отправки формSHOULD
upgrade-insecure-requestsАвтоапгрейд HTTP → HTTPSSHOULD

Примеры конфигураций

// ✅ Строгая конфигурация для новых проектов
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;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;

// ✅ Конфигурация с внешними ресурсами
const externalResourcesCSP = `
default-src 'self';
script-src 'self' 'nonce-{NONCE}' https://www.googletagmanager.com;
style-src 'self' 'nonce-{NONCE}' https://fonts.googleapis.com;
img-src 'self' data: https: blob:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com https://analytics.google.com;
frame-src https://www.youtube.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
`;

// ❌ FORBIDDEN: Небезопасная конфигурация
const unsafeCSP = `
default-src *;
script-src * 'unsafe-inline' 'unsafe-eval'; // Опасно!
style-src * 'unsafe-inline'; // Опасно!
`;

Работа с nonce в компонентах

// ✅ Использование nonce в inline-скриптах
export default function Page() {
const nonce = headers().get('x-nonce');

return (
<html>
<head>
<script nonce={nonce} dangerouslySetInnerHTML={{
__html: `
window.APP_CONFIG = {
apiUrl: '${process.env.NEXT_PUBLIC_API_URL}'
};
`
}} />
</head>
<body>
<main>{/* content */}</main>
</body>
</html>
);
}
// ✅ Использование nonce для inline-стилей
export default function Page() {
const nonce = headers().get('x-nonce');

return (
<div>
<style nonce={nonce}>{`
.custom-class {
color: red;
}
`}</style>
<div className="custom-class">Content</div>
</div>
);
}

CSP Report-Only Mode

// ✅ SHOULD: Тестирование CSP в режиме report-only
const cspReportOnly = `
default-src 'self';
script-src 'self' 'nonce-{NONCE}';
report-uri /api/csp-report;
`;

// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
// Не блокирует, только отправляет отчеты
key: 'Content-Security-Policy-Report-Only',
value: cspReportOnly,
},
],
},
];
},
};

// API endpoint для сбора отчетов
// app/api/csp-report/route.ts
export async function POST(request: Request) {
const report = await request.json();

console.error('CSP Violation:', {
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number']
});

// Отправка в систему мониторинга (Sentry, etc.)
// await sendToMonitoring(report);

return new Response('OK', { status: 200 });
}

Проверка CSP

# ✅ Проверка CSP в браузере
# 1. Откройте DevTools → Console
# 2. Посмотрите на CSP warnings/errors

# ✅ Онлайн инструменты для проверки CSP
# https://csp-evaluator.withgoogle.com/
# https://observatory.mozilla.org/

Частые ошибки

// ❌ Ошибка 1: Забыли добавить nonce к inline-скрипту
<script>
console.log('test'); // Будет заблокирован CSP!
</script>

// ✅ Правильно: Добавить nonce
const nonce = headers().get('x-nonce');
<script nonce={nonce}>
console.log('test');
</script>

// ❌ Ошибка 2: Слишком широкая директива
script-src *; // Разрешает все источники!

// ✅ Правильно: Явный whitelist
script-src 'self' 'nonce-{NONCE}' https://trusted-cdn.com;

// ❌ Ошибка 3: Использование unsafe-eval
script-src 'self' 'unsafe-eval'; // FORBIDDEN!

// ✅ Правильно: Избегайте eval и Function constructor
// Вместо eval используйте JSON.parse

Совместимость с внешними сервисами

// ✅ Google Analytics
const gaCSP = `
script-src 'self' 'nonce-{NONCE}' https://www.googletagmanager.com https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com https://analytics.google.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;
`;

// ✅ YouTube embeds
const youtubeCSP = `
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
`;

// Комбинированная конфигурация
const combinedCSP = `
default-src 'self';
script-src 'self' 'nonce-{NONCE}' https://js.stripe.com https://www.googletagmanager.com;
frame-src https://js.stripe.com https://www.youtube.com;
connect-src 'self' https://api.stripe.com https://www.google-analytics.com;
img-src 'self' data: https: blob:;
`;

📌 Ключевые моменты:

  • MUST: CSP обязателен для приложений с пользовательским контентом
  • FORBIDDEN: unsafe-inline и unsafe-eval
  • MUST: Использовать nonces для inline-скриптов
  • SHOULD: Тестировать в режиме report-only перед внедрением
  • MUST: frame-ancestors 'none' для защиты от clickjacking