Content Security Policy for Next.js: A Practical Guide

June 19, 2026

nextjssecuritycloudflare

A Content Security Policy (A browser-enforced guest list that says which sources a page is allowed to run scripts, styles, and other resources from.If an attacker injects a <script> tag pointing at evil.com, the browser refuses to load it because evil.com isn't on the page's allow-list.) sounds simple — just tell the browser which sources to trust — but it breaks in surprising ways with modern frameworks. This guide explains what it is, why Next.js fights it, and the configuration that actually works in production.

The Core Problem

Next.js injects inline scripts to hydrate your pages (turning the server-rendered HTML into a live, interactive app). A strict script-src 'self' policy blocks these inline scripts, which breaks the site entirely in production. The fix is not obvious from the CSP spec alone.

Set CSP as a production-only response header in next.config.ts:

const isDev = process.env.NODE_ENV !== "production";
 
const securityHeaders = [
  {
    key: "Content-Security-Policy",
    value: isDev
      ? "" // Disabled in dev — Next.js HMR uses inline scripts
      : [
          "default-src 'self'",
          "script-src 'self' 'unsafe-inline'",
          "style-src 'self' 'unsafe-inline'",
          "img-src 'self' data:",
          "font-src 'self'",
          "connect-src 'self'",
          "frame-ancestors 'none'",
        ].join("; "),
  },
];

Why unsafe-inline for Scripts

Next.js inline hydration scripts do not have A fresh, single-use token the server stamps on each legitimate inline script so the browser can tell real scripts from injected ones. by default. The nonce-based approach requires per-request header generation in middleware, which adds significant complexity. 'unsafe-inline' is the pragmatic choice for most applications.

If you need stricter CSP for compliance requirements, Next.js supports nonce-based CSP through middleware — but that is a separate topic.

Why CSP Is Off in Dev

The Next.js dev server relies on inline scripts for:

  • Hot Module Replacement (The dev-server feature that swaps changed code into a running page instantly, without a full reload.Change a button's color in your editor and it updates on the page right away, without wiping the form you'd half-filled out.)
  • Error overlay
  • React fast refresh

Enabling CSP in dev blocks all of these and makes development impractical. Always guard the CSP header behind a NODE_ENV !== "production" check.

Cloudflare-Specific Entries

If your site sits behind Cloudflare (even in DNS-only mode), Cloudflare may inject scripts that your CSP will block unless explicitly allowed:

  • static.cloudflareinsights.com — the Web Analytics beacon script
  • cloudflareinsights.com — the beacon data endpoint

Add both to the relevant directives:

"script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com",
"connect-src 'self' https://cloudflareinsights.com",

If you are deploying to Cloudflare Pages with a static export, put these headers in public/_headers instead of next.config.ts — the Pages edge serves those headers directly.

The Full Security Header Set

CSP alone is not enough. Pair it with:

{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{ key: "X-Frame-Options", value: "DENY" },

Each one closes a different door:

  • A response header that tells the browser to only ever connect to your site over HTTPS, never plain HTTP.Type http://yourbank.com and the browser upgrades it to https:// before sending anything, so open WiFi never carries an unencrypted request.always take the locked HTTPS road, never the open one. Once a browser has seen this header, it refuses to connect to your site over plain HTTP, even if someone tries to downgrade the connection.
  • X-Content-Type-Options: nosniffread the label on the box; don't guess what's inside. It stops the browser from second-guessing a file's type and, say, running an image as if it were a script.
  • X-Frame-Options: DENYnobody gets to frame your storefront inside theirs. It blocks other sites from embedding your pages in a hidden <iframe> to trick users into clicking things they can't see (clickjacking).

Testing Your Headers

After deploying, check your headers with Security Headers or a quick curl -I https://yourdomain.com. A correctly configured site should score A or A+ — which simply means every door above is shut and the browser is being told, explicitly, what it's allowed to do.