Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- Content Security Policy is the most powerful XSS defense available, but it is also the header most likely to break third-party integrations. This guide shows you how to build a strict CSP in Next.js that works with Stripe, Google Analytics, fonts, and more.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
How to Configure CSP in Next.js Without Breaking Stripe or Google Analytics
CSP in Next.js is one of those topics where developers start confidently, hit a wall of browser console errors, and either give up or fall back to ''unsafe-inline'' — which largely defeats the purpose.
The challenge is real: Next.js inlines scripts during hydration, third-party tools like Stripe and Google Analytics load scripts from external domains, and a CSP that is too strict silently breaks your app in production.
This guide gives you a working path from zero to a strict, nonce-based CSP that handles the most common third-party integrations.
Why CSP in Next.js Is Uniquely Difficult
Content Security Policy is an HTTP header that tells the browser exactly which scripts, styles, images, and connections are allowed. Any resource not on the allowlist is blocked.
The problem with Next.js specifically:
- 1Inline scripts: Next.js injects inline
<script>tags for hydration data and runtime configuration. A strictscript-src ''self''blocks these immediately. - 2Third-party scripts: Tools like Stripe, Google Analytics, Intercom, and Hotjar load scripts from external CDNs and make connections to their APIs.
- 3Dynamic nonce requirement: The only secure way to allow inline scripts is to use a cryptographic nonce that changes on every request — which requires server-side logic.
The naive approach is to add ''unsafe-inline'' to script-src. This re-enables inline scripts, but it also tells the browser to run any inline script on your page — including scripts injected by an XSS attacker. You have just negated most of CSP''s value.
The correct approach uses nonces.
Understanding Nonces
A nonce (number used once) is a random cryptographic token generated per request. You add it to your CSP header like this:
Content-Security-Policy: script-src ''self'' ''nonce-abc123xyz''And you add the same nonce to the specific inline script tag you want to allow:
<script nonce="abc123xyz">
// This specific script is allowed
</script>The browser only executes inline scripts whose nonce matches the one in the CSP header. Since the nonce is random and changes on every request, an attacker cannot predict it — even if they can inject HTML.
In Next.js App Router, middleware is the ideal place to generate and inject nonces.
Setting Up Nonce-Based CSP in Next.js App Router
Step 1: Generate the nonce in middleware
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
function generateNonce(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Buffer.from(array).toString("base64");
}
export function middleware(request: NextRequest) {
const nonce = generateNonce();
// Build CSP — we''ll expand this for Stripe and GA below
const csp = buildCSP(nonce);
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("content-security-policy", csp);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
response.headers.set("content-security-policy", csp);
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
};Step 2: Build the CSP string
// middleware.ts (continued)
function buildCSP(nonce: string): string {
const directives: string[] = [
"default-src ''self''",
`script-src ''self'' ''nonce-${nonce}''`,
"style-src ''self'' ''unsafe-inline''", // See note below
"img-src ''self'' data: https:",
"font-src ''self'' https://fonts.gstatic.com",
"connect-src ''self''",
"frame-src ''none''",
"frame-ancestors ''none''",
"base-uri ''self''",
"form-action ''self''",
"upgrade-insecure-requests",
];
return directives.join("; ");
}Note on `style-src ''unsafe-inline''`: Styles are less dangerous than scripts because CSS cannot exfiltrate data or execute code directly (with some exceptions). Using ''unsafe-inline'' for styles is a common pragmatic tradeoff. If you need strict style CSP, use CSS hashes for inline styles.
Step 3: Pass the nonce to your layout
// app/layout.tsx
import { headers } from "next/headers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
const nonce = headers().get("x-nonce") ?? "";
return (
<html lang="en">
<head />
<body>
{children}
</body>
</html>
);
}Next.js''s built-in <Script> component with strategy="afterInteractive" or strategy="lazyOnload" automatically picks up the nonce from the middleware-set header when configured correctly.
Adding Stripe.js to Your CSP
Stripe has detailed documentation on their CSP requirements. Here is what you need:
function buildCSP(nonce: string): string {
const directives: string[] = [
"default-src ''self''",
[
"script-src",
"''self''",
`''nonce-${nonce}''`,
"https://js.stripe.com", // Stripe.js
"https://maps.googleapis.com", // Stripe Address Element (if used)
].join(" "),
[
"frame-src",
"''self''",
"https://js.stripe.com", // Stripe Elements iframe
"https://hooks.stripe.com", // Stripe Checkout
].join(" "),
[
"connect-src",
"''self''",
"https://api.stripe.com", // Stripe API calls
"https://errors.stripe.com", // Stripe error reporting
"https://r.stripe.com", // Stripe fraud detection
].join(" "),
[
"img-src",
"''self''",
"data:",
"https:",
"https://*.stripe.com", // Stripe-hosted images
].join(" "),
"style-src ''self'' ''unsafe-inline''",
"font-src ''self'' https://fonts.gstatic.com",
"frame-ancestors ''none''",
"base-uri ''self''",
"form-action ''self''",
];
return directives.join("; ");
}Important: If you use Stripe Checkout (redirect to Stripe-hosted page), you do not need frame-src for Stripe. If you use Stripe Elements (embedded card form), you do need frame-src https://js.stripe.com.
Testing your Stripe CSP
After deploying, open Chrome DevTools, go to your checkout page, and check the Console for CSP violations. They appear as:
Refused to load the script ''https://js.stripe.com/v3/'' because it violates
the following Content Security Policy directive: "script-src ''self''"Each violation tells you exactly which domain or directive needs updating.
Adding Google Analytics (GA4) to Your CSP
GA4 loads scripts from googletagmanager.com and sends data to google-analytics.com:
function buildCSP(nonce: string): string {
const directives: string[] = [
"default-src ''self''",
[
"script-src",
"''self''",
`''nonce-${nonce}''`,
"https://js.stripe.com",
"https://www.googletagmanager.com", // GTM / GA4 script
"https://www.google-analytics.com", // GA4 older domains
"https://ssl.google-analytics.com",
].join(" "),
[
"img-src",
"''self''",
"data:",
"https:",
"https://www.googletagmanager.com", // GTM pixel
"https://www.google-analytics.com", // GA4 pixel
].join(" "),
[
"connect-src",
"''self''",
"https://api.stripe.com",
"https://errors.stripe.com",
"https://r.stripe.com",
"https://www.google-analytics.com", // GA4 data collection
"https://analytics.google.com", // GA4 data collection
"https://region1.google-analytics.com",
].join(" "),
[
"frame-src",
"''self''",
"https://js.stripe.com",
"https://hooks.stripe.com",
].join(" "),
"style-src ''self'' ''unsafe-inline''",
"font-src ''self'' https://fonts.gstatic.com",
"frame-ancestors ''none''",
"base-uri ''self''",
"form-action ''self''",
];
return directives.join("; ");
}Note on Google Fonts: If you use Google Fonts by linking to fonts.googleapis.com, you need:
- style-src ''self'' ''unsafe-inline'' https://fonts.googleapis.com
- font-src ''self'' https://fonts.gstatic.com
Alternatively, self-host your fonts (next/font does this automatically) and remove the Google Fonts entries entirely. This is the better approach — it also improves your privacy posture and page load speed.
CSP for the Pages Router
The Pages Router does not have middleware-level nonce injection as cleanly. The common approaches are:
Option A: Use `''unsafe-inline''` for scripts (lower security)
// next.config.js
const cspHeader = [
"default-src ''self''",
"script-src ''self'' ''unsafe-inline'' https://js.stripe.com https://www.googletagmanager.com",
"style-src ''self'' ''unsafe-inline''",
"img-src ''self'' data: https:",
"connect-src ''self'' https://api.stripe.com https://www.google-analytics.com",
"frame-src ''none''",
"frame-ancestors ''none''",
].join("; ");Option B: Custom server with nonce in `_document.js`
If you are running a custom Next.js server (not the default), you can generate nonces in the server middleware and pass them to _document.js via custom request context.
For most Pages Router apps without a custom server, Option A is the pragmatic choice. The nonce-based approach is the reason to migrate to App Router if strict CSP is a priority.
Using Report-Only Mode First
Never deploy a new CSP in enforcement mode to production without testing. Use Content-Security-Policy-Report-Only first:
// In middleware.ts — use report-only header during testing
response.headers.set("content-security-policy-report-only", csp);
// Also add a report-uri to collect violations
const cspWithReport = csp + "; report-uri /api/csp-violations";Create an API route to log violations:
// app/api/csp-violations/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
console.error("CSP Violation:", JSON.stringify(body, null, 2));
// In production: send to your logging service
return NextResponse.json({ received: true });
}Run your full application in staging, exercise all features (checkout flow, analytics events, font loading), and collect all violations before switching to enforcement.
Common CSP Violations and Fixes
| Violation message | Likely cause | Fix |
|---|---|---|
Refused to load script ... ''unsafe-eval'' | Next.js dev mode | Only in development — add ''unsafe-eval'' in dev builds only |
Refused to load script from ''blob:'' | PDF renderer or canvas library | Add blob: to script-src |
Refused to load frame ''https://td.doubleclick.net'' | Google Ad retargeting | Add to frame-src or remove tracking script |
Refused to connect to ''wss://...'' | WebSocket connection | Add wss: to connect-src |
Refused to load image ''data:image/...'' | Inline base64 images | Add data: to img-src |
Development vs Production CSP
Strict CSP can interfere with Next.js development tools (Fast Refresh, React error overlays). Use environment-based configuration:
// middleware.ts
const isDev = process.env.NODE_ENV === "development";
function buildCSP(nonce: string): string {
const scriptSrc = isDev
? `script-src ''self'' ''nonce-${nonce}'' ''unsafe-eval''` // Fast Refresh needs eval
: `script-src ''self'' ''nonce-${nonce}'' https://js.stripe.com https://www.googletagmanager.com`;
// ... rest of directives
}Verifying Your CSP Is Working
After deploying, scan your site with ZeriFlow. It checks whether your CSP header is present and evaluates it for common weaknesses like ''unsafe-inline'' in script-src or missing frame-ancestors. No account required.
Complement this with the Chrome DevTools Console — any CSP violation in your own browser session will be logged there in real time.
Summary
CSP in Next.js is achievable with a nonce-based approach via App Router middleware. The core pattern is: generate a random nonce per request in middleware, inject it into both the CSP header and the request headers, then read it in your layout to pass to <Script> components. Third-party integrations like Stripe and Google Analytics require specific domain allowlists in script-src, connect-src, frame-src, and img-src. Always validate with Report-Only mode before enforcing, and use zeriflow.com/free-scan to confirm your deployed configuration is correct.
Scan your Vercel deployment's security headers.
80+ checks in 60 seconds — free.