跳到內容
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 標頭解析。