Framework
Content Security Policy in Next.js
Next.js can put a nonce on every script it renders, so your policy does not need 'unsafe-inline'. It only does that for pages rendered per request, and you usually find out which pages those are from a production build.
A complete report-only config
A proxy generates a nonce per request and puts the policy on the request, where Next.js reads the nonce while it renders, and on the response, where the browser reads it. It is the Next.js documentation's pattern, in report-only mode.
Next.js 16 renamed middleware.ts to proxy.ts. On Next.js 15 and earlier, use the old file and function name.
// At the project root, or in src/. Next.js 16 renamed middleware.ts
// to proxy.ts; on Next.js 15 and earlier, name the file middleware.ts and the
// function middleware.
import { NextResponse, type NextRequest } from "next/server";
const ENDPOINT = "https://your-ingest-code.ingest.us.headerhawk.com/r";
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const isDev = process.env.NODE_ENV === "development";
const policy = [
"default-src 'self'",
// 'unsafe-eval' in development only: React's dev build uses eval for its
// error overlays. The production build does not.
`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'",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'self'",
`report-uri ${ENDPOINT}`,
"report-to csp-endpoint",
].join("; ");
// Next.js finds the nonce by reading the policy on the *request*, and stamps
// it onto its own scripts while rendering. x-nonce is for your own code:
// (await headers()).get("x-nonce") in a Server Component.
const requestHeaders = new Headers(request.headers);
requestHeaders.set("Content-Security-Policy-Report-Only", policy);
requestHeaders.set("x-nonce", nonce);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("Content-Security-Policy-Report-Only", policy);
response.headers.set("Reporting-Endpoints", `csp-endpoint="${ENDPOINT}"`);
return response;
}
export const config = {
matcher: [
{
// Pages only: static chunks, images and prefetches need no policy.
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};How this was checked: built and run in a test app on Next.js 16.3.5 with next build and next start, and under next dev. On a dynamically rendered page every framework script carries the nonce, and the browser reports only an inline script deliberately left without one; under next dev the same page produces no eval violations.
Gotcha
Statically rendered pages never get the nonce
Next.js adds the nonce while it renders, reading it from the policy on the incoming request. A page prerendered at build time was rendered before any request existed, so its HTML carries no nonce. The proxy still sends it a policy with a fresh nonce that matches nothing on the page.
With 'strict-dynamic' in script-src, 'self' stops counting, so the page's own framework chunks are all violations. In a test app on Next.js 16.3.5, the dynamically rendered page reported nothing from Next.js's scripts, and the static page beside it, behind the same proxy and policy, reported every chunk. Enforced, that page would load its HTML and none of its JavaScript.
It does not show up in development, because next dev renders every page on request. Look at the build output: ○ is static, ƒ is rendered per request.
Route (app)
┌ ƒ / dynamic: rendered per request, gets the nonce
└ ○ /pricing static: prerendered at build, gets no nonceEither opt the page into dynamic rendering, as the Next.js guide does, or keep it static and use Next.js's experimental Subresource Integrity support, which hashes scripts at build time.
// app/pricing/page.tsx
import { connection } from "next/server";
export default async function Page() {
await connection(); // render per request, so the nonce exists
// ...
}Gotcha
'unsafe-eval' belongs in development, not production
React's development build uses eval to reconstruct server-side error stacks in the browser. Under next dev, a nonce policy without 'unsafe-eval' fills the console and your reports with this. A test page with no scripts of its own produced 41 of them, plus inline-style violations from the development overlay:
Evaluating a string as JavaScript violates the following Content Security
Policy directive because 'unsafe-eval' is not an allowed source of scriptThe production build uses neither, so the config switches both on NODE_ENV. Do not add 'unsafe-eval' unconditionally because development was noisy. It passes every local check and ships a production policy that allows what script-src most needs to refuse. Check the header on the deployed site, not on localhost.
Now see what it's blocking
A report-only policy blocks nothing. When a browser loads a page and finds something the policy would have refused, it posts a report to the endpoint in report-uri and report-to. Until something listens there, those reports go nowhere.
HeaderHawk is one place to send them. It accepts both report formats on the same URL, groups them by directive and blocked host so a thousand reports about one script read as one issue, and keeps extension and bot reports visible but out of alerts. Replace your-ingest-code in the config above with the code from your site's setup panel, or paste the URL the panel shows.
The Next.js integration guide covers that step: where the endpoint goes on Next.js, and how to confirm the headers are being served.
A policy is the first of several controls browsers enforce on the code your pages load, including Integrity-Policy, Permissions-Policy, COOP and COEP, and they all report to the same endpoint. What client-side security is covers the rest of that layer.
Start reading the reports
Free for 1 site. Leave the policy in report-only until the reports go quiet, then enforce it.