csp-config-generator
CSP Config Generator
To generate a strict Content Security Policy configuration for Next.js applications, follow these steps systematically.
Step 1: Analyze Application Resources
Identify all resource types used in the application.
Discover External Resources
Use Grep to find external resource references:
Scripts:
- "<script.*src="
- "import.*from.*http"
- "next/script"
Stylesheets:
- "<link.*stylesheet"
- "@import.*url"
- "next/font"
Images:
- "<img.*src="
- "next/image"
- "background-image.*url"
Fonts:
- "@font-face"
- "next/font/google"
- "fonts.googleapis.com"
APIs and Connections:
- "fetch("
- "axios"
- "WebSocket"
Media:
- "<video"
- "<audio"
- "<iframe"
Extract Domains
Collect all unique domains used:
- CDNs (cdnjs.cloudflare.com, unpkg.com)
- APIs (api.stripe.com, *.supabase.co)
- Analytics (google-analytics.com, vercel.com)
- Fonts (fonts.googleapis.com, fonts.gstatic.com)
- Images (cloudinary.com, s3.amazonaws.com)
Consult references/csp-directives.md for directive documentation.
Step 2: Determine CSP Strategy
Choose the appropriate CSP implementation strategy:
Strategy A: Nonce-Based CSP (Recommended)
Most secure for Next.js apps with inline scripts:
- Use nonce for inline scripts and styles
- Strict directives
- Works with Next.js App Router
Strategy B: Hash-Based CSP
For static content:
- Use SHA-256 hashes for specific inline scripts
- More restrictive
- Requires rebuilding hashes on changes
Strategy C: Unsafe-Inline (Not Recommended)
Least secure, only for migration:
- Allows all inline scripts
- Use only temporarily while migrating
Recommend Strategy A (nonce-based) for modern Next.js apps.
Step 3: Generate CSP Directives
Build CSP directives based on discovered resources.
Default Directives
const cspDirectives = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'nonce-{NONCE}'", // Will be replaced dynamically
"'strict-dynamic'",
],
'style-src': [
"'self'",
"'nonce-{NONCE}'",
// Add specific domains if needed
],
'img-src': [
"'self'",
'data:',
'blob:',
// Add CDN domains
],
'font-src': [
"'self'",
'data:',
// Add font provider domains
],
'connect-src': [
"'self'",
// Add API domains
],
'frame-src': [
"'self'",
// Add allowed iframe sources
],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'frame-ancestors': ["'none'"],
'upgrade-insecure-requests': [],
}
Add Discovered Domains
For each resource type, add discovered domains:
// If using Vercel Analytics
cspDirectives['script-src'].push('https://va.vercel-scripts.com')
cspDirectives['connect-src'].push('https://vitals.vercel-insights.com')
// If using Google Fonts
cspDirectives['font-src'].push('https://fonts.gstatic.com')
cspDirectives['style-src'].push('https://fonts.googleapis.com')
// If using Supabase
cspDirectives['connect-src'].push('https://*.supabase.co')
// If using Cloudinary for images
cspDirectives['img-src'].push('https://res.cloudinary.com')
Step 4: Generate Nonce Implementation
Create nonce generation and middleware.
Generate Nonce Utility
// lib/csp/nonce.ts
import { headers } from 'next/headers'
export function generateNonce(): string {
return Buffer.from(crypto.randomUUID()).toString('base64')
}
export function getNonce(): string | undefined {
return headers().get('x-nonce') ?? undefined
}
Generate Middleware with Nonce
// 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 cspHeader = generateCSPHeader(nonce)
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set('Content-Security-Policy', cspHeader)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set('Content-Security-Policy', cspHeader)
return response
}
function generateCSPHeader(nonce: string): string {
const csp = [
{ name: 'default-src', values: ["'self'"] },
{ name: 'script-src', values: ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'"] },
{ name: 'style-src', values: ["'self'", `'nonce-${nonce}'`] },
{ name: 'img-src', values: ["'self'", 'data:', 'blob:'] },
{ name: 'font-src', values: ["'self'", 'data:'] },
{ name: 'connect-src', values: ["'self'"] },
{ name: 'frame-src', values: ["'self'"] },
{ name: 'object-src', values: ["'none'"] },
{ name: 'base-uri', values: ["'self'"] },
{ name: 'form-action', values: ["'self'"] },
{ name: 'frame-ancestors', values: ["'none'"] },
]
return csp
.map(({ name, values }) => `${name} ${values.join(' ')}`)
.join('; ')
}
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}
Update Root Layout with Nonce
// app/layout.tsx
import { getNonce } from '@/lib/csp/nonce'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const nonce = getNonce()
return (
<html lang="en">
<head nonce={nonce}>
{/* Head content will use nonce */}
</head>
<body nonce={nonce}>{children}</body>
</html>
)
}
Update Script Tags
// Use Next.js Script component with nonce
import Script from 'next/script'
import { getNonce } from '@/lib/csp/nonce'
export function AnalyticsScript() {
const nonce = getNonce()
return (
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
Step 5: Generate next.config.ts Configuration
Alternative approach using headers in next.config.ts:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: generateCSP(),
},
],
},
]
},
}
function generateCSP(): string {
const csp = [
{ name: 'default-src', values: ["'self'"] },
{
name: 'script-src',
values: [
"'self'",
"'unsafe-eval'", // Required for React dev mode
"'unsafe-inline'", // Required for Next.js hydration
// Add specific domains
'https://va.vercel-scripts.com',
],
},
{
name: 'style-src',
values: [
"'self'",
"'unsafe-inline'", // Required for Next.js styles
'https://fonts.googleapis.com',
],
},
{
name: 'img-src',
values: ["'self'", 'data:', 'blob:', 'https:'],
},
{
name: 'font-src',
values: ["'self'", 'data:', 'https://fonts.gstatic.com'],
},
{
name: 'connect-src',
values: [
"'self'",
'https://vitals.vercel-insights.com',
'https://*.supabase.co',
],
},
{ name: 'frame-src', values: ["'self'"] },
{ name: 'object-src', values: ["'none'"] },
{ name: 'base-uri', values: ["'self'"] },
{ name: 'form-action', values: ["'self'"] },
{ name: 'frame-ancestors', values: ["'none'"] },
]
// Remove 'unsafe-inline' and 'unsafe-eval' in production
if (process.env.NODE_ENV === 'production') {
const scriptSrc = csp.find(({ name }) => name === 'script-src')
if (scriptSrc) {
scriptSrc.values = scriptSrc.values.filter(
(value) => value !== "'unsafe-eval'" && value !== "'unsafe-inline'"
)
}
}
return csp
.map(({ name, values }) => `${name} ${values.join(' ')}`)
.join('; ')
}
export default nextConfig
Step 6: Handle Development vs Production
Create environment-specific CSP:
// lib/csp/config.ts
const isDevelopment = process.env.NODE_ENV === 'development'
export function getCSPDirectives() {
const baseDirectives = {
'default-src': ["'self'"],
'script-src': [
"'self'",
isDevelopment && "'unsafe-eval'", // React Fast Refresh
].filter(Boolean),
'style-src': [
"'self'",
isDevelopment && "'unsafe-inline'", // Development styles
].filter(Boolean),
// ... other directives
}
return baseDirectives
}
Step 7: Generate CSP Testing Suite
Create tests to verify CSP configuration:
// tests/csp.test.ts
import { describe, it, expect } from 'vitest'
describe('CSP Configuration', () => {
it('includes required directives', () => {
const csp = generateCSPHeader('test-nonce')
expect(csp).toContain("default-src 'self'")
expect(csp).toContain("object-src 'none'")
expect(csp).toContain('nonce-test-nonce')
})
it('does not include unsafe-inline in production', () => {
process.env.NODE_ENV = 'production'
const csp = generateCSPHeader('test-nonce')
expect(csp).not.toContain("'unsafe-inline'")
expect(csp).not.toContain("'unsafe-eval'")
})
it('allows development-specific directives', () => {
process.env.NODE_ENV = 'development'
const csp = generateCSPHeader('test-nonce')
// Development may include unsafe-eval for React Fast Refresh
expect(csp).toBeDefined()
})
})
Step 8: Generate CSP Violation Reporting
Set up CSP violation reporting:
// app/api/csp-report/route.ts
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
try {
const report = await request.json()
// Log violation
console.error('CSP Violation:', {
blockedURI: report['csp-report']['blocked-uri'],
violatedDirective: report['csp-report']['violated-directive'],
originalPolicy: report['csp-report']['original-policy'],
documentURI: report['csp-report']['document-uri'],
})
// Optionally send to monitoring service
// await sendToMonitoring(report)
return NextResponse.json({ received: true })
} catch (error) {
return NextResponse.json({ error: 'Invalid report' }, { status: 400 })
}
}
Add report-uri to CSP:
const csp = [
// ... other directives
{ name: 'report-uri', values: ['/api/csp-report'] },
{ name: 'report-to', values: ['csp-endpoint'] },
]
Step 9: Generate Documentation
Create comprehensive CSP documentation using template from assets/csp-documentation-template.md:
# Content Security Policy Configuration
## Overview
This application uses a strict Content Security Policy to prevent XSS attacks.
## Implementation
CSP is implemented via [middleware/next.config] using [nonce-based/hash-based] strategy.
## Directives
- `default-src 'self'` - Only load resources from same origin
- `script-src 'self' 'nonce-{NONCE}'` - Scripts require nonce
- [etc.]
## Adding New Resources
To add a new external resource:
1. Identify the resource type
2. Add domain to appropriate directive
3. Test in development
4. Update documentation
## Troubleshooting
Common CSP violations and fixes...
Step 10: Validate CSP Configuration
Generate validation checklist:
# CSP Validation Checklist
- [ ] All external scripts have nonce or are in script-src
- [ ] All external styles are in style-src
- [ ] All API endpoints are in connect-src
- [ ] object-src is set to 'none'
- [ ] frame-ancestors is configured
- [ ] No 'unsafe-inline' in production script-src
- [ ] CSP violations are being reported
- [ ] Development mode works correctly
- [ ] Production build passes CSP checks
Consulting References
Throughout generation:
- Consult
references/csp-directives.mdfor directive documentation - Consult
references/csp-best-practices.mdfor security guidelines - Use templates from
assets/csp-config-template.ts - Use documentation template from
assets/csp-documentation-template.md
Output Format
Generate files:
lib/csp/
nonce.ts
config.ts
middleware.ts (enhanced)
app/api/csp-report/
route.ts
docs/
csp-configuration.md
tests/
csp.test.ts
Verification Checklist
Before completing:
- All resource types analyzed
- CSP directives generated
- Nonce implementation complete
- Middleware configured
- Development/production handled
- Violation reporting set up
- Documentation created
- Tests generated
Completion
When finished:
- Display generated CSP configuration
- List all allowed domains
- Explain implementation approach
- Provide testing instructions
- Offer to implement or adjust configuration