React and Next.js give you component-level rendering control, but they do not make XSS or supply-chain injection disappear. When a third-party script, an injected snippet, or an unsafe rendering path slips through, Content Security Policy becomes one of the last browser-enforced defenses between the attacker and your users.
The old approach — listing trusted hostnames in a CSP — is often too weak for modern threat models. A single open redirect, a JSONP endpoint, or a user-controlled resource on an allowlisted CDN can bypass the entire policy. Modern guidance has shifted toward strict nonce- or hash-based CSP, and that is what this guide covers for React and Next.js applications.
TL;DR
- Modern CSP best practice is a strict nonce- or hash-based policy.
- For React and Next.js, the practical pattern is a per-request nonce generated during server-side rendering.
- Trusted root scripts get the nonce. With
strict-dynamic, scripts they load inherit trust without broad host allowlists. - Rollout should start in Report-Only mode before enforcement.
CSP is part of a broader quality-and-resilience posture. Accessibility, performance, and security all benefit from the same engineering discipline. If your team is building that muscle, see A Practical Guide to Automated Accessibility Testing for the accessibility side of the same story.
Why many current CSPs are still weak
A host-based allowlist CSP like script-src 'self' https://cdn.example.com https://www.google-analytics.com looks secure at first glance. In practice, it creates a false sense of security.
- CDN-wide trust is dangerously broad. Allowing an entire CDN domain means any resource hosted on that CDN can execute scripts in your origin. If the CDN hosts user-uploaded content or JSONP endpoints, the policy can be bypassed.
- Open redirects on allowlisted domains can be used to load scripts from attacker-controlled sources while appearing to come from a trusted host.
- Long allowlists drift and rot. Every new third-party integration adds another domain. Over time, the policy becomes so broad that it blocks very little.
This does not mean allowlisting is useless. It adds friction for attackers. But it is weaker and more bypass-prone than strict nonce- or hash-based policies. Modern guidance from Google's CSP research and OWASP has shifted accordingly.
Security headers and browser-enforced script controls are increasingly reviewed in audits and compliance assessments. Organizations handling sensitive data — whether in the EU, South Africa, or any regulated environment — should treat a weak or missing CSP as a serious AppSec concern, not a nice-to-have.
The strict CSP strategy: what actually matters
A strict CSP is built on a small number of high-value directives. Here is what matters and why.
object-src 'none'
Blocks plugin-based execution paths like Flash and Java applets. These are legacy vectors, but leaving them open is free risk with no upside in a modern React app.
base-uri 'none'
Prevents base-tag hijacking. An injected <base href="..."> tag can change how relative URLs resolve, redirecting script and resource loads to an attacker-controlled origin. Blocking this at the CSP level eliminates the vector.
script-src with nonce + strict-dynamic
This is the core of a strict CSP. The nonce is a cryptographically random value generated per request and embedded in trusted script tags. Only scripts carrying the correct nonce are authorized to execute.
strict-dynamic extends trust propagation: scripts loaded by a nonced root script inherit execution permission. This means a nonced tag manager or application bootstrap script can load its dependencies without you maintaining a fragile list of every downstream host.
How strict-dynamic interacts with allowlists
strict-dynamic, source expressions like 'self' and host-based allowlists are ignored for script execution once a valid nonce/hash + strict-dynamic is present. You may still include them as fallback tokens for older browsers, but they do not contribute to security in modern browsers.A strict policy looks something like this:
Content-Security-Policy: script-src 'nonce-<SERVER_GENERATED>' 'strict-dynamic'; object-src 'none'; base-uri 'none';
Allowlist CSP vs strict nonce-based CSP
| Approach | How it works | Main weakness | Best fit |
|---|---|---|---|
| Host allowlist | Lists trusted domains in script-src | Any resource on the allowed domain can execute; bypasses via JSONP, open redirects, or CDN-hosted user content | Legacy apps where strict CSP is not yet feasible |
| Nonce-based strict | Per-request nonce authorizes individual script tags; strict-dynamic propagates trust | Requires dynamic rendering; nonce must stay consistent per request | SSR apps (Next.js, Remix, Express-rendered React) |
| Hash-based strict | Precomputed hashes of known inline scripts authorize execution | Fragile if inline scripts change frequently; harder to manage at scale | Static sites with a small number of known inline scripts |
Implementing CSP in Next.js (App Router)
Nonce-based CSP in Next.js requires dynamic rendering because the nonce is generated and embedded during each server-side render. Statically generated pages receive the same HTML for every request, so there is no opportunity to inject a unique nonce at build time.
A) Generate the nonce in middleware
Create a cryptographically secure nonce per request and build the CSP header. Pass the nonce to downstream Server Components via a request header.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'none'",
].join("; ");
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("Content-Security-Policy", csp);
return response;
}Pro Tip
crypto.randomUUID() or crypto.getRandomValues() for nonce generation. Never use Math.random()— it is not cryptographically secure.B) Consume the nonce in the App Router
Read the nonce from the request header in your root layout and pass it to script tags that need authorization.
// app/layout.tsx
import { headers } from "next/headers";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headersList = await headers();
const nonce = headersList.get("x-nonce") ?? "";
return (
<html lang="en">
<body>
{children}
{/* Nonced script — authorized by CSP */}
<script
nonce={nonce}
src="/analytics-bootstrap.js"
/>
</body>
</html>
);
}C) Hydration and consistency
The nonce must remain consistent for the entire request lifecycle. If the server renders HTML with one nonce but the CSP header contains a different one, scripts will be blocked. Common causes of mismatch:
- Generating the nonce in multiple places instead of once in middleware
- Edge caching that serves stale HTML with an old nonce while the middleware generates a new one
- Client-side code that tries to read or regenerate the nonce after hydration
Generate once, pass everywhere, and ensure your caching layer does not cache the HTML body independently of the CSP header.
React-specific considerations
Even outside Next.js, React applications have CSP-specific friction points.
Inline scripts and event handlers
A strict CSP blocks inline scripts by default. If your codebase uses dangerouslySetInnerHTML to inject script tags, inline onclickattributes in raw HTML, or string-based script injection for analytics, those will be blocked unless nonced or hashed. React's synthetic event system avoids inline handler attributes, but raw HTML injections bypass that protection.
Client-side rendering and hydration
If your React app is purely client-rendered (no SSR), you cannot use per-request nonces because there is no server-side render pass to embed them. In that case, hash-based CSP for known inline scripts or a less strict policy may be your only option. Server-rendered React (Next.js, Remix, Express) is the natural fit for nonce-based CSP.
Analytics and deferred scripts
Analytics snippets, A/B testing scripts, and customer support widgets are often injected as inline scripts or loaded from third-party domains. Each one needs to either carry the nonce or be loaded by a nonced root script with strict-dynamicin effect. Audit every script injection path in your component tree — not just the ones in <head>.
Managing third-party scripts without blowing up your policy
Strict CSP does not mean “never use third-party scripts.” It means you need a trustworthy root execution path.
The pattern
- Give the root loader script (e.g. GTM container snippet) the request nonce.
- With
strict-dynamicactive, scripts that the nonced loader injects inherit execution trust. - You avoid maintaining an ever-growing host allowlist.
Google Tag Manager
GTM is a common pain point. The container snippet itself can be nonced. Scripts that GTM loads dynamically will execute under strict-dynamic. However, GTM also supports Custom HTML tags, which inject arbitrary inline scripts. These will be blocked unless they are loaded through a trust-propagating path. Audit what your GTM container actually executes, not just the loader.
Payments and chat widgets
Payment providers (Stripe, PayPal) and support widgets (Intercom, Zendesk) typically provide a JavaScript SDK loaded via a single script tag. Nonce that root tag and strict-dynamic handles the rest. If the provider requires inline scripts or eval-based execution, that is a signal to evaluate the provider, not to weaken your CSP.
Every third-party is an attack surface
Report-Only rollout strategy
Do not ship a strict CSP directly to production enforcement. The probability of unexpected breakage is high, especially with third-party scripts.
The rollout path
Use Content-Security-Policy-Report-Only first. This tells the browser to evaluate the policy and report violations but not block anything. You get visibility into what would break before it actually breaks.
Content-Security-Policy-Report-Only: script-src 'nonce-<value>' 'strict-dynamic'; object-src 'none'; base-uri 'none'; report-to csp-endpoint; Reporting-Endpoints: csp-endpoint="https://your-domain.com/csp-reports"
report-to is the modern reporting direction. Reporting-Endpoints defines where reports are sent. You may also include the deprecated report-uri directive for compatibility with older browsers, but report-to is the forward path.
Rollout checklist
Deploy the policy in Report-Only mode
Collect and review violation reports for at least one release cycle
Fix expected breakages (inline scripts, missing nonces, third-party conflicts)
Verify critical user flows: login, checkout, core feature paths
Move to enforcement mode for a subset of traffic or routes first
Monitor reports continuously after enforcement — new integrations can introduce violations
Common implementation pitfalls
- Treating a giant script allowlist as a “strict” CSP. If your policy lists 15 domains in
script-src, it is an allowlist, not a strict policy. The bypass surface is proportional to the number of trusted hosts. - Reusing nonces across requests. A nonce must be unique and unpredictable per request. Reusing the same value turns it into a static token that an attacker can predict and inject.
- Trying to use per-request nonces with static rendering. Static pages are built once and served to every visitor. There is no per-request context to generate a nonce. Use hash-based CSP or accept the limitation.
- Assuming React or Next.js removes the need for CSP. React's rendering model reduces some XSS vectors, but it does not eliminate them.
dangerouslySetInnerHTML, server-side injection points, and third-party scripts are all still risk surfaces. - Adding third-party scripts without revisiting the policy. Every new script integration is a CSP event. If the script does not work under your current policy, the answer is to evaluate the script, not reflexively weaken the policy.
- Relying on CSP alone while leaving unsafe rendering paths in code. CSP is a defense-in-depth layer. It limits the blast radius of an XSS bug, but it does not fix the bug. Secure coding practices eliminate the vulnerability. CSP is the backstop.
CSP is not a silver bullet
Auditing and automation
A CSP is a living control, not a one-time header. One new third-party script, one inline snippet, or one loose allowlist entry can weaken it quickly. Continuous verification matters.
- Use CodeAva Website Audit to review CSP headers and common weaknesses across your production site.
- Use the HTTP Headers Checker to verify what your production responses are actually sending. The header you think you configured and the header the browser receives are not always the same.
- Use Code Audit to catch unsafe application patterns that CSP is supposed to backstop, not replace — like unsanitized
dangerouslySetInnerHTMLusage and inline script injection.
Conclusion
CSP is most effective when designed as part of the application architecture, not bolted on after a penetration test. The path forward:
- Move from permissive allowlists toward strict nonce-based execution where your rendering stack supports it.
- Roll out in Report-Only, fix violations, then enforce.
- Treat third-party governance and reporting as ongoing engineering work, not a one-time configuration.
Start today: review your live headers with the HTTP Headers Checker, scan your site with Website Audit, and run a Code Audit to secure the broader pipeline.



