Политика безопасности контента (Content Security Policy)

Политика безопасности контента (CSP) важна для защиты вашего приложения Next.js от различных угроз безопасности, таких как межсайтовый скриптинг (XSS), кликджекинг и другие атаки путём внедрения кода.

Используя CSP, разработчики могут указать, какие источники разрешены для контента, скриптов, таблиц стилей, изображений, шрифтов, объектов, медиа (аудио, видео), iframe и других элементов.

Примеры

Nonce (Одноразовые токены)

Nonce — это уникальная случайная строка символов, создаваемая для однократного использования. Она используется вместе с CSP для выборочного разрешения выполнения определённых встроенных скриптов или стилей, обходя строгие директивы CSP.

Зачем использовать nonce?

Хотя CSP предназначены для блокировки вредоносных скриптов, существуют легитимные сценарии, когда встроенные скрипты необходимы. В таких случаях nonce предоставляет способ разрешить выполнение этих скриптов, если они содержат правильный nonce.

Добавление nonce через Middleware

Middleware позволяет добавлять заголовки и генерировать nonce перед отрисовкой страницы.

При каждом просмотре страницы должен генерироваться новый nonce. Это означает, что вы должны использовать динамическую отрисовку для добавления nonce.

Например:

import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    block-all-mixed-content;
    upgrade-insecure-requests;
`

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  requestHeaders.set(
    'Content-Security-Policy',
    // Заменяем переносы строк и пробелы
    cspHeader.replace(/\s{2,}/g, ' ').trim()
  )

  return NextResponse.next({
    headers: requestHeaders,
    request: {
      headers: requestHeaders,
    },
  })
}
import { NextResponse } from 'next/server'

export function middleware(request) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    block-all-mixed-content;
    upgrade-insecure-requests;
`

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  requestHeaders.set(
    'Content-Security-Policy',
    // Заменяем переносы строк и пробелы
    cspHeader.replace(/\s{2,}/g, ' ').trim()
  )

  return NextResponse.next({
    headers: requestHeaders,
    request: {
      headers: requestHeaders,
    },
  })
}

По умолчанию Middleware выполняется для всех запросов. Вы можете фильтровать выполнение Middleware для определённых путей, используя matcher.

Рекомендуем исключить из обработки префетчи (из next/link) и статические ресурсы, которые не требуют заголовка CSP.

export const config = {
  matcher: [
    /*
     * Обрабатывать все пути запросов, кроме начинающихся с:
     * - api (API-маршруты)
     * - _next/static (статические файлы)
     * - _next/image (файлы оптимизации изображений)
     * - favicon.ico (файл фавиконки)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}
export const config = {
  matcher: [
    /*
     * Обрабатывать все пути запросов, кроме начинающихся с:
     * - api (API-маршруты)
     * - _next/static (статические файлы)
     * - _next/image (файлы оптимизации изображений)
     * - favicon.ico (файл фавиконки)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}

Чтение nonce

Теперь вы можете прочитать nonce из Серверного Компонента с помощью headers:

import { headers } from 'next/headers'
import Script from 'next/script'

export default function Page() {
  const nonce = headers().get('x-nonce')

  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}
import { headers } from 'next/headers'
import Script from 'next/script'

export default function Page() {
  const nonce = headers().get('x-nonce')

  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}

История версий

Рекомендуем использовать Next.js версии v13.4.20+ для корректной обработки и применения nonce.