Menu

How to set a Content Security Policy (CSP) for your Next.js application

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

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

示例

Nonces

nonce 是一个唯一的、随机的字符序列,仅用于一次性使用。它与 CSP 结合使用,可以有选择地允许某些内联脚本或样式执行,从而绕过严格的 CSP 指令。

为什么使用 nonce?

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

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

使用 Proxy 添加 nonce

Proxy 使你能够在页面渲染之前添加标头并生成 nonces。

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

例如:

proxy.ts
TypeScript
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;
`
  // 替换换行符和空格
  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
}

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

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

proxy.ts
TypeScript
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下开头的路径:
     * - api(API 路由)
     * - _next/static(静态文件)
     * - _next/image(图像优化文件)
     * - favicon.ico(favicon 文件)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}

nonce 在 Next.js 中的工作原理

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

以下是 nonce 支持在动态渲染页面中的工作方式:

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

由于这种自动行为,你不需要手动将 nonce 添加到每个标签。

强制动态渲染

如果你正在使用 nonces,你可能需要明确选择页面进行动态渲染:

app/page.tsx
TypeScript
import { connection } from 'next/server'
 
export default async function Page() {
  // 等待传入请求以渲染此页面
  await connection()
  // 你的页面内容
}

读取 nonce

你可以使用 getServerSideProps 将 nonce 提供给你的页面:

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

你还可以在 _document.tsx 中访问 nonce,用于 Pages Router 应用:

pages/_document.tsx
TypeScript
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 的静态与动态渲染

使用 nonces 对你的 Next.js 应用的渲染方式有重要影响:

动态渲染要求

当你在 CSP 中使用 nonces 时,所有页面必须动态渲染。这意味着:

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

性能影响

从静态渲染转向动态渲染会影响性能:

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

何时使用 nonces

在以下情况下考虑使用 nonces:

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

不使用 Nonces

对于不需要 nonces 的应用,你可以直接在 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
TypeScript
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;
`
 
  // proxy 实现的其余部分
}

生产部署

生产环境中的常见问题:

  • nonce 未应用:确保你的 proxy 在所有必要的路由上运行
  • 静态资源被阻止:验证你的 CSP 允许 Next.js 静态资源
  • 第三方脚本:将必要的域添加到你的 CSP 策略

故障排除

第三方脚本

在 CSP 中使用第三方脚本时,确保你添加了必要的域并传递 nonce:

pages/_app.tsx
TypeScript
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://www.googletagmanager.com/gtag/js"
        strategy="afterInteractive"
        nonce={nonce}
      />
    </>
  )
}

更新你的 CSP 以允许第三方域:

proxy.ts
TypeScript
const cspHeader = `
  default-src 'self';
  script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://www.googletagmanager.com;
  connect-src 'self' https://www.google-analytics.com;
  img-src 'self' data: https://www.google-analytics.com;
`

常见的 CSP 违规

  1. 内联样式:使用支持 nonces 的 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建议用于正确的 nonce 处理和 CSP 标头解析。