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

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(), строковые аргументы в setTimeout
  • unsafe-inline без nonce
  • frame-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-srcMUSTЯвный список API endpoints
form-action 'self'SHOULDОграничение форм
upgrade-insecure-requestsSHOULDHTTP → 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-OptionsnosniffЗапрет MIME-sniffing
Referrer-Policystrict-origin-when-cross-originОграничение передачи URL в Referer
X-Frame-OptionsDENY или SAMEORIGINЗащита от clickjacking (legacy)
Permissions-Policycamera=(), microphone=()Ограничение доступа к браузерным API

Примечание: X-Frame-Options дублирует frame-ancestors CSP для поддержки старых браузеров.


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;
`;