Why Next.js needs a hand with CSP
A strong Content Security Policy is the single most effective defense against cross-site scripting. But Next.js emits inline bootstrap scripts for hydration, so a naive script-src 'self' policy blocks your own app. The clean fix is a per-request nonce combined with strict-dynamic.
Generate a nonce in middleware
Create middleware.ts at the project root. It generates a random nonce per request, puts it in the CSP header, and forwards it to the app via a request header:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = [
"default-src 'self'",
"script-src 'self' 'nonce-" + nonce + "' 'strict-dynamic'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join('; ');
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set('Content-Security-Policy', csp);
return response;
}
Use the nonce in your layout
Read the nonce from headers in your root layout. Next automatically applies it to its own framework scripts once it sees a nonce in the CSP:
import { headers } from 'next/headers';
export default async function RootLayout({ children }) {
const nonce = (await headers()).get('x-nonce') ?? '';
return (
<html>
<body>{children}</body>
</html>
);
}
Why strict-dynamic
strict-dynamic tells the browser to trust scripts loaded by an already-trusted (nonced) script, so you do not have to allow-list every CDN. It also makes host-based sources redundant, shrinking your policy and closing CDN-bypass holes.
Common gotchas
- styled-jsx and inline styles need
style-src 'unsafe-inline'(or a style nonce). Style injection is far lower risk than script, so this is an accepted trade-off. - Third-party scripts (analytics, tag managers) load fine under
strict-dynamicas long as your own nonced loader injects them. - next/image needs
img-srcto include remote image hosts anddata:for blur placeholders.
Do not forget the other headers
CSP is the headline, but pair it with HSTS, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy. The static ones can go in next.config.js under headers().
Verify it
Deploy, then scan your site with HeaderTest to confirm the CSP is present, nonce-based, and enforcing (not report-only). Re-scan after each change to watch your grade climb.