Content Security Policy (CSP) 对于保护您的 Next.js 应用程序免受各种安全威胁(例如跨站脚本 (XSS)、点击劫持和其他代码注入攻击)至关重要。
通过使用 CSP,开发者可以指定哪些来源允许用于内容源、脚本、样式表、图像、字体、对象、媒体(音频、视频)、iframe 等。
一次性随机数(nonce)是一种为单次使用而创建的独特随机字符串。它与 CSP 结合使用,以选择性地允许某些内联脚本或样式执行,从而绕过严格的 CSP 指令。
CSP 可以阻止内联和外部脚本以防止攻击。一次性随机数允许您安全地运行特定脚本——仅当它们包含匹配的 nonce值 时。
如果攻击者想将脚本加载到您的页面中,他们就需要猜测该 nonce值。这就是为什么 nonce值 必须对每个请求都是不可预测且唯一的。
Proxy 允许您在页面渲染之前添加请求头并生成一次性随机数。
每次页面被查看时,都应该生成一个新的一次性随机数。这意味着您必须使用动态渲染来添加一次性随机数。
例如:
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
}import { NextResponse } from 'next/server'
export function proxy(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;
`
// 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 过滤 Proxy 以使其在特定路径上运行。
我们建议忽略匹配的预取(来自 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' },
],
},
],
}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 在服务器端渲染期间根据请求中存在的 CSP 请求头应用一次性随机数。静态页面在构建时生成,此时不存在请求或响应头——因此无法注入一次性随机数。
以下是动态渲染页面中一次性随机数支持的工作原理:
Content-Security-Policy 请求头中,并将其设置在自定义的 x-nonce 请求头中。Content-Security-Policy 请求头,并使用 'nonce-{value}' 模式提取一次性随机数。nonce prop 的 <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
}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://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const nonce = req.headers['x-nonce']
return { props: { nonce } }
}import Script from 'next/script'
export default function Page({ nonce }) {
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
export async function getServerSideProps({ 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 MyDocumentimport Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
const nonce = ctx.req?.headers?.['x-nonce']
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您可以使用 headers 从 服务器组件 中读取一次性随机数:
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}
/>
)
}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}
/>
)
}使用一次性随机数对您的 Next.js 应用程序的渲染方式具有重要影响:
当您在 CSP 中使用一次性随机数时,所有页面都必须动态渲染。这意味着:
从静态渲染转向动态渲染会影响性能:
在以下情况下考虑使用一次性随机数:
'unsafe-inline' 的严格安全要求对于不需要一次性随机数的应用程序,您可以直接在 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, ''),
},
],
},
]
},
}作为一次性随机数的替代方案,Next.js 提供了使用子资源完整性 (SRI) 的基于哈希的 CSP 实验性支持。这种方法允许您在保持严格 CSP 的同时维护静态生成。
提示:此功能是实验性的,仅在 App Router 应用程序中使用 webpack 打包器时可用。
SRI 不使用一次性随机数,而是在构建时生成 JavaScript 文件的加密哈希。这些哈希被作为 integrity 属性添加到 script 标签中,允许浏览器验证文件在传输过程中是否被修改。
将实验性的 SRI 配置添加到您的 next.config.js 中:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
sri: {
algorithm: 'sha256', // or 'sha384' or 'sha512'
},
},
}
module.exports = nextConfig启用 SRI 后,您可以继续使用现有的 CSP 策略。SRI 通过向您的资产添加 integrity 属性来独立工作:
提示:对于动态渲染场景,如果需要,您仍然可以使用 proxy 生成一次性随机数,结合 SRI integrity 属性和基于一次性随机数的 CSP 方法。
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, ''),
},
],
},
]
},
}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
}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
}生产环境中的常见问题:
当将第三方脚本与 CSP 一起使用时:
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>
)
}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>
)
}当将第三方脚本与 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://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
</>
)
}import Script from 'next/script'
export default function App({ Component, pageProps }) {
const nonce = pageProps.nonce
return (
<>
<Component {...pageProps} />
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
</>
)
}更新您的 CSP 以允许第三方域:
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;
`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;
`script-src 策略中允许动态导入'wasm-unsafe-eval'| 版本 | 变更 |
|---|---|
v14.0.0 | 添加了基于哈希的 CSP 的实验性 SRI 支持 |
v13.4.20 | 推荐用于正确的一次性随机数处理和 CSP 请求头解析。 |