CSP for React, Vue, Angular, and Next.js: Working Examples
Table of Contents
🔧 Validate Your CSP Policy
Copy a CSP example, deploy it, then verify it works with a real scan.
Verify with headertest.com →
🔧 Validate Your CSP Policy
Copy a CSP example, deploy it, then verify it works with a real scan.
Verify with headertest.com →🔧 Validate Your CSP Policy
Copy a CSP example, deploy it, then verify it works with a real scan.
Verify with headertest.com →🔧 Validate Your CSP Policy
Copy a CSP example, deploy it, then verify it works with a real scan.
Verify with headertest.com →Single Page Applications have a unique relationship with CSP. The development workflow depends on things CSP doesn’t like — eval for HMR, inline styles for CSS-in-JS, dynamic script loading. But in production, SPAs actually benefit enormously from CSP because they’re JavaScript-heavy.
Here are configurations that work for each major framework, based on production deployments.
React (Vite)#
Vite is the recommended build tool for new React projects.
Development#
// 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'; img-src 'self' data: https: blob:; font-src 'self'; connect-src 'self' ws:; frame-ancestors 'none'"
}
}
});
You need unsafe-eval for Vite’s hot module replacement. You need ws: in connect-src for the WebSocket connection Vite uses to push HMR updates. You need blob: in img-src because some bundlers use blob URLs for certain assets.
These are development-only concessions. In production, you can be much stricter.
Production#
Deploy via Nginx or your reverse proxy:
location / {
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self'; connect-src 'self' https://api.yoursite.com; frame-ancestors 'none'; base-uri 'self'" always;
try_files $uri $uri/ /index.html;
}
React with CSS-in-JS#
If you use styled-components, emotion, or similar:
style-src 'self' 'unsafe-inline'
CSS-in-JS libraries inject <style> tags at runtime. CSP blocks these without unsafe-inline. This is the pragmatic choice — CSS can’t execute JavaScript, so the security impact is minimal.
If you want to be stricter, use the nonce support that most CSS-in-JS libraries provide:
import { StyleSheetManager } from 'styled-components';
function App({ nonce }) {
return (
<StyleSheetManager nonce={nonce}>
<YourComponent />
</StyleSheetManager>
);
}
React with Next.js (see separate section below)#
Vue.js (Vite)#
Very similar to React:
Development#
// vite.config.js
export default defineConfig({
plugins: [vue()],
server: {
headers: {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; connect-src 'self' ws:; frame-ancestors 'none'"
}
}
});
Production#
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https: blob:;
font-src 'self';
connect-src 'self' https://api.yoursite.com;
frame-ancestors 'none';
base-uri 'self'
Vue’s single-file components compile to regular JavaScript and CSS in production, so there are no special CSP concerns beyond what’s listed above.
Angular#
Angular’s build process tree-shakes and bundles everything, which makes CSP relatively straightforward:
Production (Nginx)#
location / {
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'" always;
try_files $uri $uri/ /index.html;
}
Angular with SSR (Angular Universal)#
If you’re using server-side rendering, you can generate nonces:
// server.ts
import { randomUUID } from 'crypto';
app.get('*', (req, res) => {
const nonce = Buffer.from(randomUUID()).toString('base64');
res.setHeader('Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; frame-ancestors 'none'; base-uri 'self'`
);
res.render('index', { req, res, nonce });
});
Then in your template:
<script nonce="<%= nonce %>" src="main.js"></script>
Next.js (App Router)#
Next.js has the best CSP story of any framework thanks to 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).*)', },
],
};
The matcher config is important — it excludes API routes, static files, and Next.js image optimization from CSP processing. These don’t need CSP headers and adding them would be wasteful.
Using the nonce in Next.js layouts:#
// app/layout.js
import { headers } from 'next/headers';
export default function RootLayout({ children }) {
const nonce = headers().get('x-nonce');
return (
<html lang="en">
<head>
<script nonce={nonce} src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX" />
</head>
<body>{children}</body>
</html>
);
}
Next.js Image Optimization#
Next.js uses /_next/image as a proxy endpoint. Make sure img-src includes 'self' and blob: — Next.js sometimes uses blob URLs for optimized images.
Common SPA Issues#
Service Workers#
If you use service workers (PWA), add worker-src:
worker-src 'self'
WebAssembly#
If you load .wasm modules:
script-src 'self' 'wasm-unsafe-eval'
Note: wasm-unsafe-eval only allows WebAssembly compilation. It does NOT allow JavaScript eval(). They’re separate permissions.
Third-Party Scripts Loading Dynamically#
If a third-party script loads additional scripts (like GTM loading analytics), you have two options:
- List every domain — Tedious and fragile
- Use ‘strict-dynamic’ — Scripts loaded by trusted scripts are automatically trusted
If you’re using nonces, always pair them with strict-dynamic.
Dynamic Imports#
const module = await import('./heavy-module.js');
Dynamic imports work fine with CSP as long as the chunks are served from an allowed origin. No special configuration needed — Vite, Webpack, and Next.js all serve chunks from the same origin.