Межсайтовый скриптинг (Cross-Site Scripting, XSS) остается одной из наиболее опасных угроз для веб-приложений. Даже при автоматическом экранировании строк в JSX интеграция сторонних виджетов, аналитических трекеров, полей форматированного текста (Rich Text Editor) или использование dangerouslySetInnerHTML могут привести к внедрению произвольного JavaScript-кода в контекст сессии пользователя.
Фундаментальным барьером браузерного уровня против атак внедрения является Content Security Policy (CSP). Однако при работе с Next.js App Router инженеры сталкиваются с архитектурным конфликтом:
<script> прямо в HTML-поток для прогрессивной гидратации интерфейса.'unsafe-inline'.'unsafe-inline' сводит защитную эффективность CSP практически к нулю, позволяя злоумышленнику исполнить любую XSS-полезную нагрузку.В Next.js 16 эта дилемма решается с помощью криптографических Nonce-токенов и стандарта Subresource Integrity (SRI). Сетевой периметр (proxy.ts) генерирует динамический токен на каждый входящий запрос, внедряет его в заголовок Content-Security-Policy со спецификатором 'strict-dynamic' и пробрасывает в корневой макет приложения.
Жизненный цикл криптографического nonce-токена основан на строгой синхронизации между HTTP-заголовками ответа и элементами разметки в потоке SSR. Браузер разрешает исполнение скрипта исключительно в том случае, если значение атрибута nonce="..." в DOM-элементе побайтово совпадает со значением, объявленным в заголовке Content-Security-Policy: script-src 'nonce-...'.
Поскольку злоумышленник, эксплуатирующий XSS, не может предугадать криптографически случайный токен для конкретного запроса текущей жертвы, его инжектированный код блокируется движком браузера еще до парсинга.
1. Входящий HTTP-запрос в proxy.ts:
- Генерируем crypto.randomUUID() -> "d3b07384..."
- Формируем заголовок CSP:
script-src 'nonce-d3b07384...' 'strict-dynamic';
- Пробрасываем заголовок запроса: x-nonce: d3b07384...
│
▼
2. Рендеринг Root Layout (Node.js SSR):
- const nonce = (await headers()).get('x-nonce')
- Внедрение в системные скрипты Next.js и сторонние виджеты:
<Script src="https://analytics.com/tag.js" nonce={nonce} />
│
▼
3. Получение HTML браузером:
<script nonce="d3b07384...">/* Легитимный бандл Next.js */</script>
<script>alert(document.cookie)</script> ◄── [Внедренный XSS-скрипт]
│
▼
4. Проверка движком браузера:
- Легитимный скрипт: Nonce совпадает с CSP-заголовком -> ИСПОЛНЯЕТСЯ
- XSS-скрипт: Nonce отсутствует -> БЛОКИРУЕТСЯ С ОШИБКОЙ CSPНа сетевом периметре создается псевдослучайная строка высокой энтропии с использованием Web Crypto API.
Формируется строгая политика с директивой 'strict-dynamic', позволяющей доверенным скриптам загружать свои чанки.
Сетевой прокси внедряет токен в служебный заголовок x-nonce для потребления компонентами страницы.
Корневой макет app/layout.tsx считывает токен через headers() и передает его в Next.js runtime и Script компоненты.
Браузер валидирует совпадение токенов для скриптов и проверяет SHA-хэши SRI для внешних CDN-ассетов.
Зачем в политике script-src используется директива 'strict-dynamic' совместно с криптографическим nonce?
Спецификация PCI DSS v4.0 жестко требует инспекции и контроля целостности всех клиентских скриптов, исполняемых на страницах ввода платежных данных. Строгая политика CSP с динамическими nonces гарантирует невозможность внедрения кейлоггеров и скиммеров (Magecart-атаки).
Крупные платформы вынуждены подключать рекламные пиксели и внешние трекеры. Вместо опасного разрешения целых доменов (script-src https:*), заголовок x-nonce передается в контейнер Google Tag Manager, разрешая выполнение только тем тегам, которые были явно авторизованы серверным шаблоном.
При выносе статических бандлов приложения на публичные CDN-сети (Cloudflare, AWS CloudFront) существует риск компрометации провайдера или подмены DNS. Директива experimental.sri гарантирует, что браузер откажется исполнять подмененный JS/CSS файл, если его контрольная сумма SHA-384 отличается от сборки.
С помощью директивы frame-ancestors 'none' или frame-ancestors https://partner.com политика CSP блокирует попытки отображения личного кабинета пользователя внутри прозрачных <iframe> на вредоносных фишинговых сайтах.
Сетевой прокси генерирует криптографический UUID и кодирует его в Base64.
Использует Web Crypto API без блокировки потока.
Генерируется строка Content-Security-Policy с подстановкой 'nonce-${nonce}' и 'strict-dynamic'.
Удаляются директивы 'unsafe-inline' и 'unsafe-eval'.
Токен внедряется в заголовки входящего запроса x-nonce и заголовки исходящего ответа.
Обеспечивает доступность токена на этапе SSR-рендеринга.
Root Layout читает x-nonce через headers() и проставляет атрибут nonce во все теги <Script>.
Next.js автоматически проставляет nonce в системные гидратационные скрипты.
proxy.ts#В Next.js 16 генерация nonces осуществляется на этапе прохождения сетевого периметра.
import { NextRequest, NextResponse } from 'next/server';
export default async function proxy(request: NextRequest) {
// 1. Генерация криптографически стойкого псевдослучайного nonce (128 бит)
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
// 2. Построение строгой политики CSP Level 3
// Замечание: 'strict-dynamic' игнорирует 'unsafe-inline' и домены в современных браузерах,
// обеспечивая плавную обратную совместимость со старыми клиентами CSP Level 2.
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' https:;
style-src 'self' 'nonce-${nonce}' 'unsafe-inline';
img-src 'self' blob: data: https://images.unsplash.com;
font-src 'self' data:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
.replace(/\s{2,}/g, ' ')
.trim();
// 3. Проброс nonce в заголовки запроса для чтения в Root Layout
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('Content-Security-Policy', cspHeader);
// 4. Установка заголовка CSP в исходящий HTTP-ответ
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set('Content-Security-Policy', cspHeader);
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' },
],
},
],
};app/layout.tsx#Корневой макет считывает сгенерированный токен из заголовков запроса и внедряет его в разметку и сторонние интеграции.
import { headers } from 'next/headers';
import Script from 'next/script';
import './globals.css';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Асинхронное чтение заголовков в Next.js 16
const requestHeaders = await headers();
const nonce = requestHeaders.get('x-nonce') || undefined;
return (
<html lang="ru">
<head>
{/* Next.js 16 автоматически подхватывает nonce для внутренних скриптов гидратации */}
</head>
<body className="bg-slate-950 text-slate-100 antialiased">
{children}
{/* Сторонний аналитический скрипт с явной передачей криптографического nonce */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-DEMOTRACKER"
strategy="afterInteractive"
nonce={nonce}
/>
<Script id="google-analytics-init" strategy="afterInteractive" nonce={nonce}>
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-DEMOTRACKER');
`}
</Script>
</body>
</html>
);
}next.config.ts#Для предотвращения атак на цепочку поставок статических файлов (Supply Chain Attack) Next.js поддерживает генерацию хэшей целостности для скомпилированных JS/CSS-чанков:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
// Включение генерации атрибутов integrity="sha384-..." для всех чанков сборки
sri: {
algorithm: 'sha384',
},
},
};
export default nextConfig;Что произойдет, если CDN-прокси (например, Cloudflare) закэширует HTML-страницу вместе с заголовком Content-Security-Policy, содержащим nonce?
Использование функции Math.random() или статической константы для формирования nonce делает токен угадываемым. Злоумышленник может вычислить значение генератора псевдослучайных чисел (PRNG) и внедрить валидный nonce в XSS-скрипт.
Как избежать: Используйте криптографически стойкие генераторы: crypto.randomUUID() или crypto.getRandomValues().
Если для маршрута с динамическим nonce включить статическое кэширование на CDN (Cache-Control: public, s-maxage=3600), один и тот же nonce будет отдаваться миллионам клиентов.
Как избежать: Страницы с динамической политикой nonce обязаны возвращать заголовок Cache-Control: no-store, must-revalidate либо использовать Cloudflare Workers / Fastly VCL для генерации уникального nonce на краю сети (Edge Nonce Injection).
Некоторые библиотеки стилей создают динамические теги <style> во время работы приложения. Если директива style-src не содержит 'unsafe-inline' или 'nonce-...', интерфейс отобразится без стилизации.
Как избежать: Передавайте сгенерированный токен nonce в провайдер стилей или используйте современные решения с нулевым рантаймом (Tailwind CSS, CSS Modules).
Если обратный прокси-сервер сжимает скрипты алгоритмом Brotli или удаляет комментарии (HTML minification) на лету, контрольная сумма файла изменится. Браузер отклонит загрузку файла с ошибкой Failed to find a valid digest in the 'integrity' attribute.
Как избежать: Отключайте динамическую модификацию файлов на CDN для ресурсов, использующих SRI-хэширование.