Next.js CSP rollout: how to ship Report-Only without breaking auth/analytics (checklist)
A practical CSP rollout plan for Next.js: start in Report-Only, collect violation reports, tighten allowlists, migrate scripts toward nonces/hashes, then enforce with a rollback plan.
Table of Contents
- Conclusion
- Explanation
- Practical Guide
- Step 1: inventory external dependencies (what CSP must allow)
- Step 2: ship a minimal Report-Only policy
- Step 3: add it in Next.js (global or route-scoped)
- Step 4: collect violation reports (minimal endpoint)
- Step 5: tighten allowlists by reading reports
- Step 6: move scripts toward nonces/hashes (hard mode)
- Step 7: switch from Report-Only to Enforce (with rollback)
- Pitfalls
- Checklist
- FAQ
- Q1. Should I start with a strict CSP in enforce mode?
- Q2. Is allowing https: in the beginning acceptable?
- Q3. Why is script-src always the hardest part?
- Internal links
- References
How do you roll out CSP in Next.js using Report-Only without breaking your app?
Conclusion
The safe CSP rollout path is:
- Start with CSP Report-Only (do not enforce on day 1)
- Collect violation reports and inventory real dependencies
- Tighten allowlists (move from
https:to pinned hosts) - Tackle scripts last (move toward nonces/hashes)
- Enforce only after key flows are clean, with a rollback toggle
If you skip reports, you will break auth/analytics/embeds and abandon CSP.
Explanation
CSP is not a one-time “security header”. It is a contract with the browser. In production, your app changes (tags, CDNs, embeds), and CSP must keep up.
The failure pattern is predictable:
- copy-paste a strict policy
- login or analytics breaks
- CSP gets removed
Report-Only turns CSP into an ops rollout: stage, measure, tighten, enforce.
Practical Guide
Step 1: inventory external dependencies (what CSP must allow)
List everything your app loads cross-origin:
- analytics (GA4/GTM/Segment)
- auth redirects (OAuth)
- images/CDN (S3/Cloudinary/img proxy)
- fonts (Google Fonts or self-hosted)
- embeds (YouTube/Stripe/maps)
This becomes your human-readable allowlist.
Step 2: ship a minimal Report-Only policy
Start with a policy that is strict on the obvious stuff, but permissive enough to observe.
Content-Security-Policy-Report-Only:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'none';
img-src 'self' data: https:;
script-src 'self' 'unsafe-inline' https:;
style-src 'self' 'unsafe-inline' https:;
connect-src 'self' https:;
Decision rule:
- It’s okay to allow
https:early, but only with a plan to tighten.
Step 3: add it in Next.js (global or route-scoped)
// next.config.ts
import type { NextConfig } from 'next';
const cspReportOnly = [
"default-src 'self'",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
"img-src 'self' data: https:",
"script-src 'self' 'unsafe-inline' https:",
"style-src 'self' 'unsafe-inline' https:",
"connect-src 'self' https:",
].join('; ');
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [{ key: 'Content-Security-Policy-Report-Only', value: cspReportOnly }],
},
];
},
};
export default nextConfig;
Step 4: collect violation reports (minimal endpoint)
Start by posting reports to your own endpoint.
// app/api/csp-report/route.ts
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
let body: unknown = null;
try {
body = await req.json();
} catch {
body = await req.text();
}
console.log('[csp-report]', body);
return NextResponse.json({ ok: true });
}
Add to the policy:
...; report-uri /api/csp-report;
Step 5: tighten allowlists by reading reports
Run Report-Only for days (or a week), then categorize violations:
- required (allow)
- unnecessary (remove)
- suspicious (unexpected tags/extensions/old scripts)
Practical order:
- tighten
img-src/connect-srcfirst - tighten
script-srclast
Step 6: move scripts toward nonces/hashes (hard mode)
Removing 'unsafe-inline' is the main security gain.
- prefer external scripts over inline
- use nonces/hashes where inline is unavoidable
Decision rule:
- If you cannot describe “where inline scripts come from”, you are not ready to enforce.
Step 7: switch from Report-Only to Enforce (with rollback)
Enforce when:
- key flows are clean (login, payments, forms, analytics)
- allowlists are pinned (not just
https:) - rollback is easy (feature flag / config toggle)
Switch:
Content-Security-Policy-Report-Only→Content-Security-Policy
Pitfalls
- Enforcing too early and breaking auth/analytics
- Leaving
https:open forever (never tightening) - Using
frame-ancestors 'none'while you actually need embedding - Logging too much in report endpoints (avoid PII/secrets)
Checklist
- [ ] External dependencies are inventoried (analytics/auth/cdn/fonts/embeds)
- [ ] Minimal CSP Report-Only is deployed
- [ ] Reports are collected to a controlled endpoint
- [ ] Reports are reviewed and categorized weekly
- [ ]
img-src/connect-srcare pinned to known hosts - [ ] Inline scripts are reduced or moved to external files
- [ ] Nonce/hash strategy is defined for remaining inline scripts
- [ ] Deny semantics for embed routes is decided (
frame-ancestorsscope) - [ ] Key flows are tested under the policy (login/payments/forms/analytics)
- [ ] Rollback toggle exists and is documented
- [ ] Enforcing CSP is deployed only after the above are true
FAQ
Q1. Should I start with a strict CSP in enforce mode?
No. Start with Report-Only, collect reports, then tighten. Enforcing early is how CSP projects die.
Q2. Is allowing https: in the beginning acceptable?
Yes, for observation. But it’s not a “final policy”. The goal is to pin to specific hosts.
Q3. Why is script-src always the hardest part?
Because many apps rely on inline scripts via tags, analytics, or frameworks. You need a plan (externalize, nonce, or hashes) before enforcing.
Internal links
- Parent hub: AI development: start here
- Related:
References
- MDN: Content Security Policy: https://developer.mozilla.org/docs/Web/HTTP/CSP
- MDN: CSP violation reports: https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
Popular
- 1Permit2 explained (Web3): why approvals changed and how to use it safely (checklist)
- 2Read wallet signing screens (Web3): a 30-second checklist to avoid permission traps
- 3Spec-to-implementation prompt template (AI development): how to stop the model from guessing
- 4Revoke token approvals on EVM: how to audit allowances safely (checklist)
- 5Clarifying questions checklist (AI development): what to ask before you let an LLM build