内容安全策略(Content Security Policy,CSP)有助于保护 Next.js 应用,抵御跨站脚本 XSS、点击劫持及其他代码注入攻击。
通过 CSP,开发者可以规定脚本、样式表、图片、字体、对象、音视频媒体、iframe 等各类内容允许来自哪些来源。
完整示例参阅 Strict CSP。
Nonce
Nonce 是为单次使用生成的、唯一且随机的字符串。它与 CSP 配合,在严格策略下选择性地允许某些内联脚本或样式执行。
为什么使用 nonce
CSP 可以阻止内联脚本和外部脚本,降低攻击风险。Nonce 则允许特定脚本安全运行,前提是它携带的 nonce 值与策略中规定的值匹配。
攻击者若想向页面注入可运行脚本,就必须猜中 nonce。因此 nonce 必须不可预测,而且每个请求都不同。
通过 Proxy 添加 nonce
Proxy 可以在页面渲染前添加请求头并生成 nonce。
每次查看页面都应生成新的 nonce,因此,添加 nonce 必须使用动态渲染。
补充说明:开发环境需要 'unsafe-eval',因为 React 使用 eval 提供更丰富的调试信息,例如在浏览器中重建服务端错误堆栈。生产环境不需要它;React 与 Next.js 默认都不会在生产环境使用 eval。
下面分别给出 TypeScript 与 JavaScript 示例:
proxy.ts
import { NextRequest, NextResponse } from 'next/server'
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' '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
}
proxy.js
import { NextResponse } from 'next/server'
export function proxy(request) {
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' '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
}
Proxy 默认作用于所有请求。可以通过 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' },
],
},
],
}
proxy.js
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 如何处理 nonce
页面必须动态渲染,因为 Next.js 会在服务端渲染期间,根据请求中的 CSP 头添加 nonce。静态页面是在构建时生成的,当时没有请求或响应头,因此无法注入每个请求特有的 nonce。
动态页面的处理过程如下:
- Proxy 生成 nonce:为请求生成唯一 nonce,把它加入
Content-Security-Policy,同时写入自定义x-nonce请求头。 - Next.js 提取 nonce:渲染时解析 CSP 头,按
'nonce-{value}'模式提取值。 - 自动应用:Next.js 将 nonce 加到框架脚本,包括 React 与 Next.js 运行时、页面专用 JavaScript 包、Next.js 生成的内联样式和脚本,以及使用
nonce属性的<Script>组件。
由于存在这套自动处理机制,无需手动给每个标签添加 nonce。
强制动态渲染
使用 nonce 时,可能需要显式让页面采用动态渲染:
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
}
app/page.jsx
import { connection } from 'next/server'
export default async function Page() {
// wait for an incoming request to render this page
await connection()
// Your page content
}
读取 nonce
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}
/>
)
}
CSP 与静态、动态渲染
使用 nonce 会对 Next.js 应用的渲染方式产生重要影响。
动态渲染要求
在 CSP 中使用 nonce 时,采用该策略的页面必须动态渲染。这意味着:
- 页面可能构建成功,但如果未正确配置动态渲染,仍可能在运行时出错。
- 每个请求都会生成带新 nonce 的新页面。
- 静态优化和增量静态再生成 ISR 不再适用。
- 没有额外配置时,页面不能由 CDN 缓存。
- 局部预渲染 PPR 与基于 nonce 的 CSP 不兼容,因为静态外壳中的脚本无法获得该 nonce。
性能影响
- 首次加载较慢:每个请求都需要生成页面。
- 服务器负载增加:每次请求都要服务端渲染。
- 默认没有 CDN 缓存:动态页面不能直接在边缘缓存。
- 托管成本上升:动态渲染需要更多服务器资源。
何时考虑 nonce
- 安全要求严格,不允许使用
'unsafe-inline'。 - 应用处理敏感数据。
- 需要允许特定内联脚本,同时阻止其他内联脚本。
- 合规要求强制采用严格 CSP。
不使用 nonce
如果应用不需要 nonce,可以直接在 next.config.js 中设置 CSP 头:
next.config.js
const isDev = process.env.NODE_ENV === 'development'
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ''};
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, ''),
},
],
},
]
},
}
子资源完整性 SRI:实验性功能
作为 nonce 的替代方案,Next.js 提供实验性的子资源完整性 Subresource Integrity(SRI)支持,用于基于哈希的 CSP。该方式允许保留静态生成,同时使用严格的内容安全策略。
注意:这是一项实验功能,适用于 App Router 应用。
SRI 的工作方式
SRI 不使用 nonce,而是在构建时为 JavaScript 文件生成加密哈希,并以 integrity 属性的形式添加到 script 标签。浏览器可借此确认资源在传输过程中没有被修改。
启用 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 属性独立工作。
补充说明:在动态渲染场景中,如有需要,仍可通过 Proxy 生成 nonce,将 SRI 完整性属性与基于 nonce 的 CSP 配合使用。
next.config.js
const isDev = process.env.NODE_ENV === 'development'
const cspHeader = `
default-src 'self';
script-src 'self'${isDev ? " 'unsafe-eval'" : ''};
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 相比 nonce 的优势
- 静态生成:页面可静态生成并缓存。
- 兼容 CDN:静态页面可以使用 CDN 缓存。
- 性能更好:无需每次请求都服务端渲染。
- 构建时完整性:在构建阶段生成哈希,用于验证文件完整性。
SRI 的限制
- 仍属实验功能,未来可能变化或被移除。
- 仅支持 App Router,不支持 Pages Router。
- 只在构建时工作,无法处理动态生成的脚本。
开发环境与生产环境的差异
开发环境
开发时需要启用 'unsafe-eval'。React 使用 eval 增强调试,例如在浏览器重建服务端错误堆栈,显示错误在服务器上的起点。
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
}
proxy.js
export function proxy(request) {
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
}
生产部署
常见问题包括:
- nonce 未应用:确认 Proxy 覆盖所有必要路由。
- 静态资源被拦截:确认 CSP 允许 Next.js 静态资源。
- 第三方脚本被拦截:将必需的域名加入相应 CSP 指令。
故障排查
第三方脚本
在 CSP 下使用第三方脚本时,可像下面这样将 nonce 传给 Google Tag Manager 组件:
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>
)
}
app/layout.jsx
import { GoogleTagManager } from '@next/third-parties/google'
import { headers } from 'next/headers'
export default async function RootLayout({ children }) {
const nonce = (await headers()).get('x-nonce')
return (
<html lang="en">
<body>
{children}
<GoogleTagManager gtmId="GTM-XYZ" nonce={nonce} />
</body>
</html>
)
}
随后更新策略,允许所需第三方域名:
proxy.ts
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;
`
proxy.js
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 违规
- 内联样式:使用支持 nonce 的 CSS-in-JS 库,或把样式移动到外部文件。
- 动态导入:确保
script-src允许动态导入所需脚本。 - WebAssembly:使用 WebAssembly 时添加
'wasm-unsafe-eval'。 - Service Worker:为服务工作线程脚本添加合适策略。
版本历史
| 版本 | 变化 |
|---|---|
v14.0.0 | 为基于哈希的 CSP 增加实验性 SRI 支持。 |
v13.4.20 | 原文推荐该版本用于正确处理 nonce 与解析 CSP 请求头。 |
相关参考:proxy.js 文件 API、headers 函数 API。
原文:How to set a Content Security Policy (CSP) for your Next.js application。本文为该文档的中文译文,示例代码保留原文。











暂无评论内容