跳到内容
App Router指南内容安全策略

如何为你的 Next.js 应用程序设置内容安全策略 (CSP)

内容安全策略 (CSP) 对于保护你的 Next.js 应用程序免受各种安全威胁(例如跨站脚本 (XSS)、点击劫持和其他代码注入攻击)至关重要。

通过使用 CSP,开发人员可以指定哪些来源是内容源、脚本、样式表、图像、字体、对象、媒体(音频、视频)、iframe 等的允许来源。

示例

随机数

随机数 是一个用于一次性使用的独特、随机字符串。它与 CSP 结合使用,选择性地允许某些内联脚本或样式执行,从而绕过严格的 CSP 指令。

为什么要使用随机数?

CSP 可以阻止内联和外部脚本以防止攻击。随机数允许您安全地运行特定脚本——仅当它们包含匹配的随机数值时。

如果攻击者想在您的页面中加载脚本,他们需要猜测随机数值。这就是为什么随机数必须是不可预测且对每个请求都是唯一的。

使用代理添加随机数

代理 允许您在页面渲染之前添加标头并生成随机数。

每次查看页面时,都应生成一个新的随机数。这意味着您必须使用动态渲染来添加随机数

例如

proxy.ts
import { NextRequest, NextResponse } from 'next/server'
 
export function proxy(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;
`
  // Replace newline characters and spaces
  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
}

默认情况下,代理在所有请求上运行。您可以使用 matcher 过滤代理以在特定路径上运行。

我们建议忽略匹配的预取(来自 next/link)和不需要 CSP 标头的静态资产。

proxy.ts
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}

Next.js 中的随机数工作原理

要使用随机数,您的页面必须是动态渲染的。这是因为 Next.js 在服务器端渲染期间应用随机数,基于请求中存在的 CSP 标头。静态页面在构建时生成,此时不存在请求或响应标头——因此无法注入随机数。

以下是动态渲染页面中随机数支持的工作原理:

  1. 代理生成随机数:您的代理为请求创建一个唯一的随机数,将其添加到您的 Content-Security-Policy 标头中,并将其设置为自定义的 x-nonce 标头。
  2. Next.js 提取随机数:在渲染期间,Next.js 解析 Content-Security-Policy 标头,并使用 'nonce-{value}' 模式提取随机数。
  3. 随机数自动应用:Next.js 将随机数附加到
    • 框架脚本(React、Next.js 运行时)
    • 页面特定的 JavaScript 包
    • Next.js 生成的内联样式和脚本
    • 任何使用 nonce 属性的 <Script> 组件

由于这种自动行为,您无需手动将随机数添加到每个标签。

强制动态渲染

如果您正在使用随机数,您可能需要明确选择页面进行动态渲染

app/page.tsx
import { connection } from 'next/server'
 
export default async function Page() {
  // wait for an incoming request to render this page
  await connection()
  // Your page content
}

读取随机数

您可以使用 headers服务器组件 中读取随机数

app/page.tsx
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://#/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}

使用 CSP 的静态与动态渲染

使用随机数对 Next.js 应用程序的渲染方式有重要影响

动态渲染要求

当您在 CSP 中使用随机数时,所有页面都必须动态渲染。这意味着

  • 页面将成功构建,但如果未正确配置为动态渲染,可能会遇到运行时错误
  • 每个请求都会生成一个带有新随机数的新页面
  • 静态优化和增量静态再生 (ISR) 将被禁用
  • 页面无法在没有额外配置的情况下被 CDN 缓存
  • 部分预渲染 (PPR) 与基于随机数的 CSP 不兼容,因为静态 shell 脚本无法访问随机数

性能影响

从静态渲染到动态渲染的转变会影响性能

  • 初始页面加载速度变慢:页面必须在每个请求上生成
  • 服务器负载增加:每个请求都需要服务器端渲染
  • 无 CDN 缓存:动态页面默认无法在边缘缓存
  • 托管成本更高:动态渲染需要更多的服务器资源

何时使用随机数

在以下情况下考虑使用随机数

  • 您有严格的安全要求,禁止使用 'unsafe-inline'
  • 您的应用程序处理敏感数据
  • 您需要允许特定的内联脚本,同时阻止其他脚本
  • 合规性要求强制执行严格的 CSP

不使用随机数

对于不需要随机数的应用程序,您可以直接在 next.config.js 文件中设置 CSP 标头

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 提供对使用子资源完整性 (SRI) 的基于哈希的 CSP 的实验性支持。这种方法允许您在保持静态生成的同时拥有严格的 CSP。

须知:此功能是实验性的,仅在 App Router 应用程序中与 webpack 打包器一起提供。

SRI 工作原理

SRI 不使用随机数,而是在构建时生成 JavaScript 文件的加密哈希。这些哈希作为 integrity 属性添加到脚本标签中,允许浏览器验证文件在传输过程中是否被修改过。

启用 SRI

将实验性 SRI 配置添加到您的 next.config.js

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    sri: {
      algorithm: 'sha256', // or 'sha384' or 'sha512'
    },
  },
}
 
module.exports = nextConfig

使用 SRI 的 CSP 配置

启用 SRI 后,您可以继续使用现有的 CSP 策略。SRI 通过为您的资产添加 integrity 属性独立工作

须知:对于动态渲染场景,如果需要,您仍然可以通过代理生成随机数,结合 SRI 完整性属性和基于随机数的 CSP 方法。

next.config.js
const cspHeader = `
    default-src 'self';
    script-src 'self';
    style-src 'self';
    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 = {
  experimental: {
    sri: {
      algorithm: 'sha256',
    },
  },
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: cspHeader.replace(/\n/g, ''),
          },
        ],
      },
    ]
  },
}

SRI 相较于随机数的优势

  • 静态生成:页面可以静态生成和缓存
  • CDN 兼容性:静态页面与 CDN 缓存兼容
  • 更好的性能:每个请求不需要服务器端渲染
  • 构建时安全:在构建时生成哈希,确保完整性

SRI 的局限性

  • 实验性:功能可能会更改或移除
  • 仅支持 Webpack:不支持 Turbopack
  • 仅支持 App Router:Pages Router 不支持
  • 仅限于构建时:无法处理动态生成的脚本

开发与生产环境注意事项

CSP 的实现在开发环境和生产环境之间存在差异

开发环境

在开发环境中,您需要启用 'unsafe-eval' 以支持提供额外调试信息的 API

proxy.ts
export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const isDev = process.env.NODE_ENV === 'development'
 
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ''};
    style-src 'self' ${isDev ? "'unsafe-inline'" : `'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;
`
 
  // Rest of proxy implementation
}

生产部署

生产中的常见问题

  • 随机数未应用:确保您的代理在所有必要的路由上运行
  • 静态资产被阻止:验证您的 CSP 允许 Next.js 静态资产
  • 第三方脚本:将必要的域添加到您的 CSP 策略中

故障排除

第三方脚本

当与 CSP 一起使用第三方脚本时

app/layout.tsx
import { GoogleTagManager } from '@next/third-parties/google'
import { headers } from 'next/headers'
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const nonce = (await headers()).get('x-nonce')
 
  return (
    <html lang="en">
      <body>
        {children}
        <GoogleTagManager gtmId="GTM-XYZ" nonce={nonce} />
      </body>
    </html>
  )
}

更新您的 CSP 以允许第三方域

proxy.ts
const cspHeader = `
  default-src 'self';
  script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://#;
  connect-src 'self' https://#;
  img-src 'self' data: https://#;
`

常见 CSP 违规

  1. 内联样式:使用支持随机数的 CSS-in-JS 库或将样式移至外部文件
  2. 动态导入:确保您的 script-src 策略允许动态导入
  3. WebAssembly:如果使用 WebAssembly,请添加 'wasm-unsafe-eval'
  4. Service Workers:为 Service Worker 脚本添加适当的策略

版本历史

版本更改
v14.0.0为基于哈希的 CSP 添加了实验性 SRI 支持
v13.4.20推荐用于正确的随机数处理和 CSP 标头解析。