跳到内容
页面路由指南内容安全策略

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

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

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

示例

随机数

随机数 (nonce) 是一个独特、随机的字符串,用于一次性使用。它与 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
}

读取随机数

您可以使用 getServerSideProps 将随机数提供给您的页面

pages/index.tsx
import Script from 'next/script'
 
import type { GetServerSideProps } from 'next'
 
export default function Page({ nonce }) {
  return (
    <Script
      src="https://#/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}
 
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
  const nonce = req.headers['x-nonce']
  return { props: { nonce } }
}

您还可以在 Pages Router 应用程序的 _document.tsx 中访问随机数

pages/_document.tsx
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
  DocumentInitialProps,
} from 'next/document'
 
interface ExtendedDocumentProps extends DocumentInitialProps {
  nonce?: string
}
 
class MyDocument extends Document<ExtendedDocumentProps> {
  static async getInitialProps(
    ctx: DocumentContext
  ): Promise<ExtendedDocumentProps> {
    const initialProps = await Document.getInitialProps(ctx)
    const nonce = ctx.req?.headers?.['x-nonce'] as string | undefined
 
    return {
      ...initialProps,
      nonce,
    }
  }
 
  render() {
    const { nonce } = this.props
 
    return (
      <Html lang="en">
        <Head nonce={nonce} />
        <body>
          <Main />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    )
  }
}
 
export default MyDocument

使用 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, ''),
          },
        ],
      },
    ]
  },
}

开发与生产环境考量

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 结合使用时,请确保添加必要的域并传递随机数

pages/_app.tsx
import type { AppProps } from 'next/app'
import Script from 'next/script'
 
export default function App({ Component, pageProps }: AppProps) {
  const nonce = pageProps.nonce
 
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://#/gtag/js"
        strategy="afterInteractive"
        nonce={nonce}
      />
    </>
  )
}

更新您的 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. 动态导入:确保脚本源策略中允许动态导入
  3. WebAssembly:如果使用 WebAssembly,请添加 'wasm-unsafe-eval'
  4. 服务工作线程:为服务工作线程脚本添加适当的策略

版本历史

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