如何为 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 标头解析。 |
这有帮助吗?