Как настроить политику безопасности контента (CSP) для приложения Next.js

Политика безопасности контента (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';
    upgrade-insecure-requests;
`
  // Заменяем переносы строк и пробелы
  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, ' ')
    .trim()

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)

  requestHeaders.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue
  )

  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
  response.headers.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue
  )

  return response
}
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';
    upgrade-insecure-requests;
`
  // Заменяем переносы строк и пробелы
  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, ' ')
    .trim()

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  requestHeaders.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue
  )

  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
  response.headers.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue
  )

  return response
}

По умолчанию 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 в Server Component с помощью headers:

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

export default async function Page() {
  const nonce = (await 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 async function Page() {
  const nonce = (await headers()).get('x-nonce')

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

Без использования Nonce

Для приложений, которые не требуют nonce, вы можете установить заголовок CSP напрямую в файле next.config.js:

next.config.js
const cspHeader = `
    default-src 'self';
    script-src 'self' 'unsafe-eval' 'unsafe-inline';
    style-src 'self' 'unsafe-inline';
    img-src 'self' blob: data:;
    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, ''),
          },
        ],
      },
    ]
  },
}

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

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