Add a Content Security Policy to a Next.js app — a simple static policy in next.config, a per-request nonce generated in proxy.ts, reading the nonce in a Server Component, and scoping it with a matcher.
Why: a Content Security Policy is a response header listing exactly which sources of scripts, styles, and other content the browser may load — even if an attacker injects a <script>, the browser refuses to run it unless your policy allows it. The easiest version is one static header. default-src 'self' restricts everything to your own origin, object-src 'none' blocks <object>/<embed>, and frame-ancestors 'none' stops other sites from iframing you. Use this when you have no inline scripts that need a nonce.
// next.config.ts
import type { NextConfig } from 'next'
const csp = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"img-src 'self' data:",
"object-src 'none'",
"frame-ancestors 'none'",
].join('; ')
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [{ key: 'Content-Security-Policy', value: csp }],
},
]
},
}
export default nextConfigNote: a stricter policy allows an inline script only if it carries a one-time random token — a nonce. Generate a fresh nonce per request in proxy.ts (this Next.js version's successor to middleware.ts), put it in the script-src directive, and pass it to the page via a custom x-nonce header. Next.js then attaches the nonce to its own scripts automatically. Note: using a nonce forces dynamic rendering, so static optimization is disabled.
// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"frame-ancestors 'none'",
].join('; ')
const headers = new Headers(request.headers)
headers.set('x-nonce', nonce) // pass the nonce to the page
const res = NextResponse.next({ request: { headers } })
res.headers.set('Content-Security-Policy', csp)
return res
}Where: when you add your own inline or third-party <Script>, give it the nonce so the browser allows it. Read the x-nonce header in a Server Component with the async headers() helper and pass it to next/script.
// app/page.tsx — a Server Component
import { headers } from 'next/headers'
import Script from 'next/script'
export default async function Page() {
const nonce = (await headers()).get('x-nonce') ?? undefined
return (
<Script
src="https://example.com/widget.js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}Note: proxy runs on every request by default, which is wasteful for static assets and link prefetches. A matcher limits it to real page navigations — skip the API, Next.js internals, and the favicon, and skip prefetch requests so a fresh nonce is only generated for actual page loads.
// proxy.ts
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}