如何为你的 Next.js 应用程序设置内容安全策略(CSP)
内容安全策略(CSP)对于保护你的 Next.js 应用程序免受各种安全威胁(如跨站脚本攻击(XSS)、点击劫持和其他代码注入攻击)非常重要。
通过使用 CSP,开发者可以指定哪些来源对于内容源、脚本、 样式表、图片、字体、对象、媒体(音频、视频)、iframe 等是可接受的。
示例
Nonces
nonce 是为一次性使用而创建的唯一的随机字符串。它与 CSP 结合使用,可以有选择地允许某些内联脚本或样式执行,绕过严格的 CSP 指令。
为什么使用 nonce?
尽管 CSP 旨在阻止恶意脚本,但在某些合法场景下内联脚本是必要的。在这种情况下,nonce 提供了一种方式,如果脚本具有正确的 nonce,就允许这些脚本执行。
使用中间件添加 nonce
中间件使你能够在页面渲染之前添加头部信息并生成 nonce。
每次查看页面时,都应该生成一个新的 nonce。这意味着你必须使用动态渲染来添加 nonce。
例如:
- TypeScript
 - JavaScript
 
middleware.ts
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
}
middleware.js
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
}
默认情况下,中间件在所有请求上运行。你可以使用 matcher 过滤中间件以在特定路径上运行。
我们建议忽略匹配预取(来自 next/link)和不需要 CSP 头部的静态资源。
- TypeScript
 - JavaScript
 
middleware.ts
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' },
      ],
    },
  ],
}
middleware.js
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
- TypeScript
 - JavaScript
 
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://www.googletagmanager.com/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}
app/page.jsx
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 的应用程序,你可以直接在 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, ''),
          },
        ],
      },
    ]
  },
}
版本历史
我们建议使用 v13.4.20+ 的 Next.js 来正确处理和应用 nonce。