Next.js CSP rollout: how to ship Report-Only without breaking auth/analytics (checklist)
securitywebops

Next.js CSP rollout: how to ship Report-Only without breaking auth/analytics (checklist)

4 min read

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

How do you roll out CSP in Next.js using Report-Only without breaking your app?

Conclusion

The safe CSP rollout path is:

  1. Start with CSP Report-Only (do not enforce on day 1)
  2. Collect violation reports and inventory real dependencies
  3. Tighten allowlists (move from https: to pinned hosts)
  4. Tackle scripts last (move toward nonces/hashes)
  5. 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:

  1. tighten img-src / connect-src first
  2. tighten script-src last

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-OnlyContent-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-src are 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-ancestors scope)
  • [ ] 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.

References

Popular

  1. 1Permit2 explained (Web3): why approvals changed and how to use it safely (checklist)
  2. 2Read wallet signing screens (Web3): a 30-second checklist to avoid permission traps
  3. 3Spec-to-implementation prompt template (AI development): how to stop the model from guessing
  4. 4Revoke token approvals on EVM: how to audit allowances safely (checklist)
  5. 5Clarifying questions checklist (AI development): what to ask before you let an LLM build

Related Articles