如何为你的 Next.js 应用程序设置内容安全策略(CSP)
内容安全策略(CSP)对于保护你的 Next.js 应用程序免受各种安全威胁(如跨站脚本攻击(XSS)、点击劫持和其他代码注入攻击)非常重要。
通过使用 CSP,开发者可以指定哪些来源对于内容源、脚本、样式表、图片、字体、对象、媒体(音频、视频)、iframe 等是可接受的。
示例
Nonces
nonce 是为一次性使用而创建的唯一的随机字符串。它与 CSP 结合使用,可以有选择地允许某些内联脚本或样式执行,绕过严格的 CSP 指令。
为什么使用 nonce?
尽管 CSP 旨在阻止恶意脚本,但在某些合法场景下内联脚本是必要的。在这种情况下,nonce 提供了一种方式,如果脚本具有正确的 nonce,就允许这些脚本执行。
使用中间件添加 nonce
中间件使你能够在页面渲染之前添加头部信息并生成 nonce。
每次查看页面时,都应该生成一个新的 nonce。这意味着你必须使用动态渲染来添加 nonce。
例如:
- TypeScript
- JavaScript
middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(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;
`
// 替换换行符和空格
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
}
middleware.js
import { NextResponse } from 'next/server'
export function middleware(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;
`
// 替换换行符和空格
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 头部的静态资源。
- TypeScript
- JavaScript
middleware.ts
export const config = {
matcher: [
/*
* 匹配所有请求路径,除了以以下开头的:
* - api(API 路由)
* - _next/static(静态文件)
* - _next/image(图片优化文件)
* - favicon.ico(favicon 文件)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}
middleware.js
export const config = {
matcher: [
/*
* 匹配所有请求路径,除了以以下开头的:
* - api(API 路由)
* - _next/static(静态文件)
* - _next/image(图片优化文件)
* - favicon.ico(favicon 文件)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}
读取 nonce
你可以使用
getServerSideProps
向你的页面提供 nonce:
- TypeScript
- JavaScript
pages/index.tsx
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 } }
}
pages/index.jsx
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 } }
}
- TypeScript
- JavaScript
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}
/>
)
}
不使用 Nonce
对于不需要 nonce 的应用程序,你可以直接在 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, ''),
},
],
},
]
},
}
版本历史
我们建议使用 v13.4.20+
的 Next.js 来正确处理和应用 nonce。