Content Security Policy in Next.js: Static Pages, Nonces, and Real‑World Trade‑Offs

Image by Bruce Barrow.

In Brief

A useful CSP in Next.js is a tradeoff between security, rendering mode, framework scripts, thirdparty tools, previews, and staticpage constraints. Inventory what the page loads before choosing headers, nonces, or broad allowances such as unsafe-inline.

Content Security Policy is one of those security features that sounds simple until it meets a real web application.

The principle is good: tell the browser which sources are allowed for scripts, styles, images, frames, fonts, connections, and other resource types. Reduce the blast radius of crosssite scripting and injection attacks. Make dangerous surprises harder to execute.

Then the real site appears. Next.js injects framework scripts. Analytics needs to load. A consent tool wants inline configuration. A map embed uses an iframe. The CMS serves images from a CDN. Marketing wants a tag manager. Preview needs to work in another environment. Some pages are static. Some are dynamic. Suddenly the policy is either too weak to be meaningful or too strict to ship.

CSP is worth doing, but it is not a pastein header.


A CSP is a Browser Contract

A Content Security Policy is enforced by the browser. The server sends a header, and the browser uses it to decide which resources may load or execute.

That makes CSP different from many applicationlevel checks. It does not replace input validation, output encoding, authentication, or dependency hygiene. It adds a browserside boundary that can reduce damage if something slips through.

The Next.js documentation frames Content Security Policy as protection against threats such as crosssite scripting, clickjacking, and code injection. That is the right level of ambition. CSP is not a magic shield. It is one layer in a defensive system.

The practical question is how strict the policy can be without breaking the application or teaching everyone to ignore violations.


Static Pages Change the Nonce Conversation

Nonces are a common way to allow specific inline scripts or styles. The server generates a onetime value, includes it in the CSP header, and attaches the matching nonce to approved scripts.

That works naturally when a page is rendered per request. It becomes awkward for static pages because the page is generated at build time, before there is a requestspecific nonce. The Next.js CSP guide explains this directly: static pages are generated at build time, when no request or response headers exist, so nonce injection is not available in the same way.

That tradeoff matters for Next.js sites because static generation is often the right choice for performance, SEO, and reliability. Forcing a static content site into dynamic rendering only to support noncebased CSP may be a poor exchange.

There are three broad options:

  • keep static generation and use a CSP model that does not depend on request nonces
  • make specific routes dynamic where strict nonce behaviour is worth the cost
  • redesign inline script usage so the policy can stay simpler

For staticheavy routes, that may mean moving inline code into external files, using CSP hashes for genuinely static inline blocks, or evaluating Next.js's experimental Subresource Integrity support where it fits.

Those are related approaches, but they are not interchangeable. CSP hashes can authorise known content, whilst Next.js's current SRI support adds integrity information to buildtime JavaScript assets. At the time of writing, that support remains experimental, is limited to the App Router with webpack, and does not cover dynamically generated scripts.

MDN's script-src reference remains a useful grounding point because it explains the distinction between nonce sources, hash sources, strict-dynamic, and broad allowances such as unsafe-inline.

The wrong answer is pretending static and dynamic pages have the same CSP constraints.


Start by Inventorying What the Page Actually Loads

Before writing a policy, inspect the resources.

Which scripts load from the application itself? Which come from analytics, consent, tag managers, maps, video embeds, chat widgets, or monitoring tools? Which images come from the CMS or image CDN? Which API origins are used by browserside code? Which frames are embedded? Which styles are inline? Which fonts are served locally or remotely?

This is another place where CSP overlaps with performance work. A routelevel thirdparty script inventory helps security as well as Core Web Vitals. If nobody can list the origins, nobody can write a meaningful policy.

The first version of the policy should often run in reportonly mode. That lets the team see violations without breaking the site immediately. Reportonly is not the finish line, but it is a useful way to separate expected resource usage from accidental usage.


Third‑Party Scripts are the Hard Part

The easiest CSP is the one with no thirdparty scripts. Most production sites do not have that luxury.

Tag managers are especially awkward because they are designed to load and configure other scripts. A policy that allows the tag manager but not the scripts it injects will break tags. A policy that allows everything the tag manager might ever need becomes broad enough to lose much of its value.

That is not a reason to give up. It is a reason to govern the tag manager.

Ask whether every thirdparty origin is needed on every route. Ask whether inline configuration can move to a safer pattern. Ask whether a vendor supports nonce attributes, external configuration, serverside tagging, or more restricted domains. Ask whether the same script is being loaded through both application code and a tag manager.

Security policy should force these conversations. If it cannot, the policy will gradually become a list of exceptions nobody trusts.


Avoid Unsafe Directives Unless You Know the Cost

Many broken CSP implementations are fixed by adding broad allowances. unsafe-inline makes inline scripts easier. unsafe-eval helps libraries that compile or evaluate code at runtime. Wide wildcard sources stop things breaking when vendors change URLs.

There is one Next.jsspecific distinction worth making here. Development and production do not necessarily need the same policy.

Current Next.js development tooling requires unsafe-eval for some of React's enhanced debugging behaviour. That does not mean unsafe-eval should automatically appear in the production policy; React and Next.js do not require it there by default.

A development concession should therefore be exactly that: environmentspecific, understood, and kept out of production unless the production application has a separate, explicit reason for it.

Sometimes a temporary relaxation is necessary to keep a site working whilst the team migrates. It should still be treated as debt.

The problem is not that one directive is morally bad. The problem is that each broad allowance weakens the browser contract. If the goal is to reduce injection risk, the policy should not permanently allow the very behaviour it was meant to constrain.

Careful routelevel work helps. An admin route, a marketing page, an article page, and an embedded preview route may not need identical policy. The closer the policy matches the actual route, the less pressure there is to make it universally loose.


Next.js Scripts Need Deliberate Handling

Next.js gives teams a Script component for loading scripts with defined strategies. The scripts guide notes that additional attributes, including nonce, can be forwarded to the final script element. That is useful, but it does not remove the need to understand the rendering model.

For dynamically rendered pages, nonceaware scripts can fit neatly into a strict policy. For static pages, the same pattern may not apply. For thirdparty scripts loaded globally in a root layout or custom app, every route inherits the CSP and performance cost.

Script strategy, route ownership, and CSP should be designed together. If a script is not needed until after interaction, it should not force a weaker policy during initial render. If a script is only needed on one route, it should not expand the CSP for the whole site.


For Dynamic Pages, the Nonce Must Reach the Request

For dynamically rendered pages, there is an important detail in how the nonce reaches Next.js. It is not enough to generate a nonce and add the finished CSP only to the response.

Next.js applies nonces during serverside rendering by reading the CSP header from the incoming request. Proxy therefore needs to make both the nonce and the policy available before rendering begins:

import { NextRequest, NextResponse } from 'next/server';export function proxy(request: NextRequest) {  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');  const cspHeader = `    default-src 'self';    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';    style-src 'self' 'nonce-${nonce}';    img-src 'self' blob: data:;    font-src 'self';    object-src 'none';    base-uri 'self';    form-action 'self';    frame-ancestors 'none';    upgrade-insecure-requests;  `;  const contentSecurityPolicy = cspHeader    .replace(/\s{2,}/g, ' ')    .trim();  const requestHeaders = new Headers(request.headers);  requestHeaders.set('x-nonce', nonce);  requestHeaders.set(    'Content-Security-Policy',    contentSecurityPolicy,  );  const response = NextResponse.next({    request: {      headers: requestHeaders,    },  });  response.headers.set(    'Content-Security-Policy',    contentSecurityPolicy,  );  return response;}

Here, the request and response headers are doing two different jobs.

The CSP on the forwarded request is available to Next.js while it renders the page. Next.js can parse the nonce from that policy and apply it to the framework scripts, pagespecific JavaScript, and inline scripts and styles it generates. The separate x-nonce header also makes the same value available to application code through Next.js's headers() API when it needs to be passed explicitly, for example to a Script component.

The CSP on the response is what the browser eventually receives and enforces.

If the policy is set only on the outgoing response, it arrives too late for Next.js to use that nonce during rendering. The browser would receive the policy, but the framework would already have generated the page without having had the corresponding nonce available during the render.

This is why noncebased CSP and dynamic rendering are closely connected in Next.js: the nonce has to exist before the page renders, not merely before the response reaches the browser.


Search and Answer Engines Still Need the Page to Work

CSP mistakes can look like security work whilst quietly breaking discovery.

If a policy blocks image CDN assets, structured data injection, preview scripts, consent behaviour, analytics, fonts, or browserside data requests, the visible page may still seem close enough during a quick check. The rendered output can still be weaker than intended. Images disappear. Layout shifts. Product or article context fails to load. Internal widgets stop exposing links. Monitoring loses the signal that would have shown the problem.

Search engines and answer engines need stable rendered pages, not just strict headers. A CSP that improves security whilst damaging crawlable content, metadata, or structured data has not been designed carefully enough.

That does not mean weakening the policy whenever a vendor complains. It means checking important templates after enforcement: rendered text, canonical metadata, JSONLD parseability, images, forms, embeds, and routespecific scripts. Security controls should support the site's purpose, not accidentally obscure it.


Frames, Previews, and Admin Tools Need Special Care

Frame rules are often discovered late.

A strict frame-ancestors directive can prevent the site being embedded elsewhere, which is usually good. It can also break CMS live preview if the CMS needs to frame the page. A loose frame policy can make clickjacking easier. Preview domains, staging domains, CMS origins, and production domains need to be considered explicitly.

This is why preview should not be an afterthought. If Contentful preview, Draft Mode, or internal QA tooling needs embedded routes, the CSP has to allow those routes safely rather than accidentally.

The same applies to connect-src. Browserside preview tools, monitoring, analytics, and APIs may all need outbound connections. List them intentionally.


Make CSP Deployment Incremental

Do not ship a strict CSP across a large existing site in one blind release.

A safer route is:

  1. Inventory resources on important templates.
  2. Add a reportonly policy.
  3. Review violations and remove dead resources.
  4. Tighten obvious sources.
  5. Move inline scripts or styles where practical.
  6. Decide which pages need dynamic nonce support and which should stay static.
  7. Enforce the policy on a limited route set.
  8. Expand once violations are understood.

That approach sounds slower. It is faster than breaking analytics, checkout, preview, or consent in production and then deleting the policy under pressure.


CSP Should Fail Loudly Enough to Maintain

A policy nobody monitors will rot.

When CSP blocks a script, the team needs a path for seeing and triaging the violation. Some violations are attacks or browser extensions. Some are legitimate regressions. Some are vendor changes. Some are old tools nobody knew still existed.

Without monitoring, the policy becomes either too permissive to matter or too brittle to keep.

Good CSP work leaves behind ownership:

  • who reviews new origins
  • who owns tag manager changes
  • how reportonly violations are triaged
  • how preview and staging differ from production
  • when a broad allowance must be revisited

Security controls need maintenance just like performance budgets and dependency updates.


Wrapping Up

Content Security Policy in Next.js is not difficult because the header syntax is complicated. It is difficult because modern sites load a lot of code from a lot of places, and Next.js can render pages in several different ways.

Static pages, nonces, thirdparty scripts, tag managers, CMS preview, and routespecific behaviour all affect the shape of a workable policy.

The useful CSP is not the strictest header someone can write in isolation. It is the strongest policy the application can actually operate, monitor, and improve.

Key Takeaways

  • CSP is a browserside security layer, not a replacement for application security.
  • Static Next.js pages cannot use requestspecific nonce behaviour in the same way dynamic pages can.
  • Thirdparty scripts and tag managers should be inventoried before writing policy.
  • Broad allowances such as unsafe-inline should be treated as debt, not default architecture.
  • CSP rollout should be incremental, monitored, and owned.

Want to find out more?

If you need senior handson support with a complex React or Next.js platform, migration, performance issue, or technical SEO problem, send me the context and I'll tell you where I can help.