Securityhighheaders
Implement a content security policy
rule · content-security-policy
Content Security Policy (CSP) is your primary defense against XSS attacks by controlling what resources browsers can load.
Code Example
HTTP
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self';Why It Matters
Content Security Policy prevents cross-site scripting (XSS), clickjacking, and data injection attacks by controlling which resources can be loaded and executed on your pages.
Sanitize Untrusted HTML Before Rendering
CSP reduces exploitability, but it should never be the only protection when you render rich text from a CMS, markdown pipeline, comments system, or WYSIWYG editor. Sanitize untrusted HTML before it reaches a DOM sink such as innerHTML or dangerouslySetInnerHTML.
TSX
import DOMPurify from 'dompurify'
function RichContent({ html }: { html: string }) {
const safeHtml = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
})
return <div dangerouslySetInnerHTML={{ __html: safeHtml }} />
}CSP Directives Overview
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for all fetch directives | 'self' |
script-src | JavaScript sources | 'self' 'nonce-abc123' |
style-src | CSS sources | 'self' 'unsafe-inline' |
img-src | Image sources | 'self' data: https: |
connect-src | Fetch, XHR, WebSocket | 'self' <trusted-api-origin> |
font-src | Font file sources | 'self' <approved-font-origin> |
frame-src | iframe sources | 'none' |
object-src | Plugins (Flash, etc.) | 'none' |
base-uri | <base> tag URLs | 'self' |
form-action | Form submission targets | 'self' |
Server Configuration
Next.js
TypeScript
// next.config.ts
import type { NextConfig } from 'next'
const generateCSP = () => {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join('; ')
return { csp, nonce }
}
const config: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: generateCSP().csp,
},
],
},
]
},
}
export default configNext.js Middleware with Nonce
TypeScript
// 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 = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' blob: data: https:`,
`font-src 'self'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
`upgrade-insecure-requests`,
].join('; ')
const response = NextResponse.next()
response.headers.set('Content-Security-Policy', cspHeader)
response.headers.set('x-nonce', nonce)
return response
}Nginx
NGINX
# /etc/nginx/snippets/csp.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self';" always;
# In server block
server {
listen 443 ssl http2;
server_name example.com;
include snippets/csp.conf;
# ... rest of config
}Apache
APACHE
# .htaccess or httpd.conf
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"
</IfModule>Using Nonces for Inline Scripts
TSX
// app/layout.tsx
import { headers } from 'next/headers'
import Script from 'next/script'
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const headersList = await headers()
const nonce = headersList.get('x-nonce') || ''
return (
<html lang="en">
<head>
<Script
nonce={nonce}
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
// Your inline script here
console.log('Loaded with nonce');
`,
}}
/>
</head>
<body>{children}</body>
</html>
)
}Using Hashes for Static Inline Scripts
TypeScript
// Generate hash for inline script
import crypto from 'crypto'
const script = `console.log('Hello, CSP!');`
const hash = crypto
.createHash('sha256')
.update(script)
.digest('base64')
// Use in CSP header
const csp = `script-src 'self' 'sha256-${hash}'`CSP for Common Third-Party Services
HTTP
# Google Analytics + Google Fonts + YouTube embeds
Content-Security-Policy:
default-src 'self';
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com;
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
object-src 'none';
base-uri 'self';Report-Only Mode for Testing
HTTP
# Test CSP without breaking anything
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report; report-to csp-endpoint;CSP Report Endpoint
TypeScript
// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
const report = await request.json()
// Log CSP violation
console.error('CSP Violation:', JSON.stringify(report, null, 2))
// Send to monitoring service
// await sendToMonitoring(report)
return NextResponse.json({ received: true })
} catch {
return NextResponse.json({ error: 'Invalid report' }, { status: 400 })
}
}Progressive CSP Implementation
Phase 1: Report-Only
HTTP
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;Phase 2: Relaxed Enforcement
HTTP
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';Phase 3: Strict Enforcement
HTTP
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self';Trusted Types for Larger Apps
Applications with many DOM injection points can add Trusted Types as an extra guardrail around script-creating sinks:
HTTP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' 'strict-dynamic';
object-src 'none';
base-uri 'self';
require-trusted-types-for 'script';
trusted-types app dompurify;TypeScript
// Example Trusted Types policy for a large app
const policy = window.trustedTypes?.createPolicy('app', {
createHTML: (input) => DOMPurify.sanitize(input, {
RETURN_TRUSTED_TYPE: true,
}) as unknown as string,
})Additional Security Headers
TypeScript
// Complete security headers
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: csp,
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
]Testing CSP
Shell
# Check CSP header
curl -I https://example.com | grep -i content-security-policy
# Test with browser DevTools
# Open Console - CSP violations appear as errors
# Open Network tab - check response headersOnline Tools
- CSP Evaluator (Google) (opens in a new tab)
- Security Headers (opens in a new tab)
- Mozilla Observatory (opens in a new tab)
Common CSP Mistakes
| Mistake | Risk | Fix |
|---|---|---|
unsafe-inline for scripts | XSS attacks | Use nonces or hashes |
unsafe-eval | Code injection | Avoid eval(), new Function() |
Overly permissive * | Data exfiltration | Specify exact domains |
Missing object-src 'none' | Flash exploits | Always set to 'none' |
Missing base-uri | Base tag injection | Set to 'self' |
| Rendering raw untrusted HTML | DOM XSS | Sanitize before innerHTML |
| No Trusted Types on large app shell | Unsafe DOM sinks remain reachable | Add require-trusted-types-for 'script' where supported |
Exceptions
- A missing or weak header should be evaluated against the live production response path, not only the framework or server config in isolation.
- Legacy integrations or embedded third-party content may require narrowly scoped exceptions, but they should be documented explicitly instead of left permissive by default.
- When multiple security headers are missing, prioritize the header that removes the highest exploitability or browser capability first.
Standards
- Align the implementation with OWASP: HTTP Headers Cheat Sheet and verify the effective response or browser behavior, not only the configuration file.
- Align the implementation with MDN: Web security and verify the effective response or browser behavior, not only the configuration file.
Support Notes
- The feature is supported across the current project browser matrix.
- Baseline-compatible minimums: chrome 115, edge 115, firefox 116, safari 16.4, safari_ios 16.4.
- Add a fallback or a narrower policy note when a required project target falls outside that support range.
Verification
Automated Checks
- Test the affected flow in a production-like environment, not just local development.
- Document any intentional exceptions explicitly.
Manual Checks
- Inspect the final HTTP response or browser behavior to confirm the control is actually enforced.
- Verify third-party integrations or embeds still work after the restriction is applied.
- Attempt one representative rich-content render and confirm the HTML is sanitized before insertion.
- In larger apps, confirm Trusted Types violations appear during testing when unsafe DOM sinks are used.