如何為 Next.js 應用程式設定內容安全策略 (CSP)
內容安全策略 (CSP) 對於保護您的 Next.js 應用程式免受跨站指令碼 (XSS)、點選劫持和其他程式碼注入攻擊至關重要。
透過使用 CSP,開發人員可以指定哪些來源是內容源、指令碼、樣式表、影像、字型、物件、媒體(音訊、影片)、iframe 等的允許來源。
示例
隨機數
隨機數 (nonce) 是一個獨特、隨機的字串,用於一次性使用。它與 CSP 結合使用,以選擇性地允許某些內聯指令碼或樣式執行,從而繞過嚴格的 CSP 指令。
為什麼要使用隨機數?
CSP 可以阻止內聯和外部指令碼以防止攻擊。隨機數允許您安全地允許特定指令碼執行——前提是它們包含匹配的隨機數值。
如果攻擊者想將指令碼載入到您的頁面中,他們需要猜測隨機數值。這就是為什麼隨機數必須是不可預測且每個請求唯一的。
使用代理新增隨機數
代理使您能夠在頁面渲染之前新增標頭並生成隨機數。
每次頁面被檢視時,都應該生成一個新的隨機數。這意味著您必須使用動態渲染來新增隨機數。
例如
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 標頭的靜態資產。
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 標頭應用隨機數。靜態頁面在構建時生成,此時不存在請求或響應標頭——因此無法注入隨機數。
以下是隨機數支援在動態渲染頁面中的工作原理
- 代理生成隨機數:您的代理為請求建立一個唯一的隨機數,將其新增到您的
Content-Security-Policy標頭中,並將其設定在自定義的x-nonce標頭中。 - Next.js 提取隨機數:在渲染期間,Next.js 解析
Content-Security-Policy標頭並使用'nonce-{value}'模式提取隨機數。 - 隨機數自動應用:Next.js 將隨機數附加到
- 框架指令碼(React、Next.js 執行時)
- 頁面特定的 JavaScript 包
- Next.js 生成的內聯樣式和指令碼
- 任何使用
nonce屬性的<Script>元件
由於這種自動行為,您無需手動為每個標籤新增隨機數。
強制動態渲染
如果您正在使用隨機數,您可能需要明確選擇頁面進行動態渲染
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 將隨機數提供給您的頁面
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 中訪問隨機數
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 標頭
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
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 結合使用時,請確保新增必要的域並傳遞隨機數
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 以允許第三方域
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://#;
connect-src 'self' https://#;
img-src 'self' data: https://#;
`常見 CSP 違規
- 內聯樣式:使用支援隨機數的 CSS-in-JS 庫或將樣式移至外部檔案
- 動態匯入:確保指令碼源策略中允許動態匯入
- WebAssembly:如果使用 WebAssembly,請新增
'wasm-unsafe-eval' - 服務工作執行緒:為服務工作執行緒指令碼新增適當的策略
版本歷史
| 版本 | 更改 |
|---|---|
v14.0.0 | 為基於雜湊的 CSP 添加了實驗性 SRI 支援 |
v13.4.20 | 建議用於正確的隨機數處理和 CSP 標頭解析。 |
這有幫助嗎?