React and Next.js make CSP harder than it needs to be. The development workflow assumes you can run inline scripts, eval is baked into Webpack’s hot module replacement, and your CSS-in-JS library is generating styles that CSP doesn’t like.

But you can make it work. Here’s how, based on what I’ve seen actually work in production.

The Core Challenge

React apps commonly do things that CSP hates:

  • Inline scripts for initial state hydration
  • CSS-in-JS libraries (styled-components, emotion) generating inline styles
  • Webpack HMR using eval() in development
  • Third-party scripts (analytics, A/B testing) loaded dynamically

The good news: in production, most of these issues go away. HMR doesn’t exist, scripts get bundled, and you have a build step that can generate nonces.

Next.js (App Router) — The Cleanest Approach

Next.js with the App Router has first-class CSP support through middleware:

// middleware.js
import { NextResponse } from 'next/server';
import { randomUUID } from 'crypto';

export function middleware(request) {
  const nonce = Buffer.from(randomUUID()).toString('base64');
  
  const cspHeader = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' blob: data: https:",
    "font-src 'self'",
    `connect-src 'self' https://api.yoursite.com`,
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
  ].join('; ');

  const response = NextResponse.next();
  response.headers.set('x-nonce', nonce);
  response.headers.set('Content-Security-Policy', cspHeader);
  return response;
}

export const config = {
  matcher: [
    { source: '/((?!api|_next/static|_next/image|favicon.ico).*)', },
  ],
};

Then use the nonce in your layout:

// app/layout.js
import { headers } from 'next/headers';

export default function RootLayout({ children }) {
  const nonce = headers().get('x-nonce');
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

For third-party scripts that need a nonce, create a wrapper component:

// components/Script.js
import { headers } from 'next/headers';
import Script from 'next/script';

export function TrustedScript({ src, ...props }) {
  const nonce = headers().get('x-nonce');
  return <Script src={src} nonce={nonce} {...props} />;
}

Next.js (Pages Router)

The Pages Router doesn’t have middleware in the same way, so you use the config:

// next.config.js
const cspHeader = [
  "default-src 'self'",
  "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
  "style-src 'self' 'unsafe-inline'",
  "img-src 'self' blob: data: https:",
  "font-src 'self'",
  "connect-src 'self'",
  "frame-ancestors 'none'",
].join('; ');

module.exports = {
  async headers() {
    return [{
      source: '/(.*)',
      headers: [{
        key: 'Content-Security-Policy',
        value: cspHeader,
      }],
    }];
  },
};

Note the unsafe-eval and unsafe-inline here — this is the pragmatic approach for Pages Router because it’s harder to inject nonces. It’s not ideal, but you still get frame protection, connect restrictions, and base-uri protection.

Create React App

CRA doesn’t give you server-side control over headers. Your options:

If deploying to Netlify: Create a public/_headers file:

C o n t e n t - S e c u r i t y - P o l i c y : d e f a u l t - s r c ' s e l f ' ; s c r i p t - s r c ' s e l f ' ; s t y l e - s r c ' s e l f ' ' u n s a f e - i n l i n e ' ; i m g - s r c ' s e l f ' d a t a : h t t p s : ; f r a m e - a n c e s t o r s ' n o n e '

If deploying to Vercel: Create a vercel.json:

{
  "headers": [{
    "source": "/(.*)",
    "headers": [{"key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'"}]
  }]
}

If using your own server (Nginx, Express): Set the header there, which gives you more control.

Vite + React

Vite makes it easier because you control the server:

// vite.config.js
export default defineConfig({
  plugins: [react()],
  server: {
    headers: {
      'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'"
    }
  }
});

In production, remove unsafe-eval since HMR isn’t needed:

// For production builds, use a stricter policy
const isDev = process.env.NODE_ENV === 'development';
const cspScriptSrc = isDev 
  ? "'self' 'unsafe-eval'" 
  : "'self'";

CSS-in-JS and style-src

Styled-components, emotion, and other CSS-in-JS libraries generate inline <style> tags at runtime. CSP blocks these by default.

Your options:

  1. Use ‘unsafe-inline’ for style-src — This is what most teams do. The risk is low since CSS can’t execute JavaScript. I’d recommend this unless you have strict compliance requirements.

  2. Use server-side rendering — If you’re using Next.js with SSR, styled-components can extract styles to a <style> tag in the initial HTML. Still technically inline, but it’s the initial render, not runtime injection.

  3. Use the styled-components nonce prop — styled-components supports a nonce prop through its StyleSheetManager:

import { StyleSheetManager } from 'styled-components';

function App({ nonce }) {
  return (
    <StyleSheetManager nonce={nonce}>
      <YourApp />
    </StyleSheetManager>
  );
}

Common Pitfalls

Webpack HMR — In development, Webpack uses eval() for hot module replacement. You’ll need 'unsafe-eval' in dev. That’s fine — use a different policy for development and production.

Next.js Image Optimization — Next.js uses /_next/image as a proxy for image optimization. Make sure your img-src includes 'self' and blob: (Next.js uses blob URLs for some image operations).

Dynamic importsReact.lazy() and import() work fine with CSP as long as the chunks are served from an allowed origin. No special configuration needed.

Source maps — If you use source maps in production (you probably shouldn’t), they might load from unexpected paths. Check your CSP reports.

Third-party scripts loaded by other third-party scripts — This is where 'strict-dynamic' saves you. With strict-dynamic, scripts loaded by a trusted script are automatically trusted. Without it, you’d need to whitelist every sub-resource individually.

Verify Your Setup