Content Security Policy Configuration Guide
How to implement Content Security Policy headers that actually protect you: directive syntax, nonce-based inline scripts, reporting and safe rollout.

Content Security Policy (CSP) is the strongest defense against Cross-Site Scripting (XSS). It tells the browser which sources of content are allowed — scripts, styles, images, fonts, and connections. If an attacker injects a script tag into your page, the browser blocks it because the script source is not in your CSP allowlist.
Most teams skip CSP because they think it is too complex or will break their site. The reality is that a properly configured CSP can be deployed incrementally — start with report-only mode, fix violations, then enforce. This guide walks through the process from zero to full enforcement.
CSP Directive Basics
A CSP is a set of directives, each controlling a resource type. Each directive specifies which sources are allowed.
// The most common CSP directives
const cspDirectives = {
"default-src":
"Fallback for all resource types not explicitly listed",
"script-src":
"JavaScript sources (most critical for XSS prevention)",
"style-src":
"CSS sources",
"img-src":
"Image sources",
"font-src":
"Font sources",
"connect-src":
"URLs for fetch(), XHR, WebSocket, EventSource",
"frame-src":
"Sources for <iframe> and <frame>",
"media-src":
"Audio and video sources",
"object-src":
"Plugin sources (<object>, <embed>, <applet>)",
"base-uri":
"URLs allowed in <base> element",
"form-action":
"URLs that forms can submit to",
"frame-ancestors":
"Who can embed this page (replaces X-Frame-Options)",
"report-uri":
"Where to send violation reports (deprecated, use report-to)",
"report-to":
"Reporting endpoint group for violations",
};# ❌ Overly permissive CSP that provides little protection
Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval';
# Allows everything from everywhere — XSS protection is effectively disabled
# ✅ Strict CSP that actually protects against XSS
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'nonce-abc123';
img-src 'self' data: https://images.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
report-to csp-violations;Nonce-Based CSP for Inline Scripts
The unsafe-inline keyword defeats CSP for scripts entirely — any injected inline script runs. Nonces solve this: each page load generates a unique random token. Only script tags with the matching nonce execute.
import crypto from "crypto";
import { Request, Response, NextFunction } from "express";
function cspMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
// Generate a unique nonce per request
const nonce = crypto.randomBytes(16).toString("base64");
// Make nonce available to templates
res.locals.cspNonce = nonce;
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https://images.example.com`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join("; ");
res.setHeader("Content-Security-Policy", csp);
next();
}<!-- In your HTML template -->
<!DOCTYPE html>
<html>
<head>
<!-- ❌ Blocked: inline script without nonce -->
<script>console.log("This is blocked by CSP")</script>
<!-- ✅ Allowed: inline script with matching nonce -->
<script nonce="<%= cspNonce %>">
window.__CONFIG__ = { apiUrl: "/api" };
</script>
<!-- ✅ Allowed: external script from 'self' -->
<script src="/js/app.js"></script>
<!-- ❌ Blocked: external script from unknown origin -->
<script src="https://evil.com/steal-data.js"></script>
<!-- ✅ Allowed: style with nonce -->
<style nonce="<%= cspNonce %>">
body { font-family: sans-serif; }
</style>
</head>
<body>
<!-- If an attacker injects via XSS: -->
<!-- <script>document.cookie</script> -->
<!-- Browser blocks it: no nonce, no execution -->
</body>
</html>Incremental Rollout with Report-Only Mode
Deploying CSP enforcement immediately on a complex site will break things. Report-only mode sends violation reports without blocking content, so you can identify and fix issues first.
interface CSPViolationReport {
"csp-report": {
"document-uri": string;
"violated-directive": string;
"blocked-uri": string;
"source-file": string;
"line-number": number;
"column-number": number;
"original-policy": string;
};
}
// Step 1: Deploy in report-only mode
function reportOnlyCSP(nonce: string): string {
return [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data:`,
`connect-src 'self'`,
`report-uri /api/csp-report`,
].join("; ");
}
// Set report-only header (does NOT block anything)
// res.setHeader("Content-Security-Policy-Report-Only", reportOnlyCSP(nonce));
// Step 2: Collect and analyze violation reports
import express from "express";
const app = express();
app.post(
"/api/csp-report",
express.json({ type: "application/csp-report" }),
(req, res) => {
const report = req.body as CSPViolationReport;
const violation = report["csp-report"];
console.log("CSP violation:", {
page: violation["document-uri"],
directive: violation["violated-directive"],
blocked: violation["blocked-uri"],
source: violation["source-file"],
line: violation["line-number"],
});
// Store violations for analysis
// Group by blocked-uri to identify patterns
res.status(204).end();
}
);// Step 3: Fix violations iteratively
// Common violations and their fixes:
const violationFixes = {
"Inline script blocked": {
violation: "script-src: 'inline'",
fix: "Add nonce to the script tag, or move to external file",
},
"Inline style blocked": {
violation: "style-src: 'inline'",
fix: "Add nonce to style tag, or use external stylesheet. " +
"For style attributes, use 'unsafe-hashes' with specific hashes",
},
"Third-party script blocked": {
violation: "script-src: https://cdn.analytics.com",
fix: "Add the domain to script-src directive",
},
"eval() blocked": {
violation: "script-src: 'eval'",
fix: "Refactor code to avoid eval(). If a library requires it, " +
"add 'unsafe-eval' only for that specific context (last resort)",
},
"data: URI blocked": {
violation: "img-src: data:",
fix: "Add 'data:' to img-src if you use base64 images",
},
};
// Step 4: When violations drop to near-zero, switch to enforcement
// res.setHeader("Content-Security-Policy", enforceCSP(nonce));CSP for Single-Page Applications
SPAs have specific CSP challenges: dynamic script loading, CSS-in-JS libraries, and API calls to multiple backends.
// Next.js CSP configuration
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 csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
// 'strict-dynamic' trusts scripts loaded by already-trusted scripts
// If app.js (trusted via nonce) loads chunk-abc.js, it's allowed
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: blob: https://images.example.com`,
`font-src 'self'`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`object-src 'none'`,
`base-uri 'self'`,
].join("; ");
const response = NextResponse.next();
response.headers.set("Content-Security-Policy", csp);
// Pass nonce to the page via request header
response.headers.set("x-nonce", nonce);
return response;
}// ❌ CSS-in-JS libraries often use inline styles
// styled-components, emotion, etc. inject <style> tags at runtime
// Without nonce support, they break under strict CSP
// ✅ Configure CSS-in-JS to use nonces
// styled-components: <StyleSheetManager nonce={nonce}>
// emotion: <CacheProvider value={createCache({ nonce })}>
// If the library doesn't support nonces, consider:
// 1. Using CSS modules instead (no runtime injection)
// 2. Using 'unsafe-inline' for styles only (still protects scripts)
// 3. Extracting styles at build time (next/styled-components plugin)
const cspForCSSInJS = {
withNonce: `style-src 'self' 'nonce-${nonce}'`,
fallback: `style-src 'self' 'unsafe-inline'`,
// 'unsafe-inline' for styles is acceptable when script-src is strict
// XSS payload injection is via scripts, not style attributes
};Testing Your CSP
# Verify CSP headers are set correctly
curl -s -D - https://example.com | grep -i content-security-policy
# Use Google's CSP Evaluator to check for weaknesses
# https://csp-evaluator.withgoogle.com/
# Common mistakes that weaken CSP:
# 1. Using 'unsafe-inline' for script-src (defeats XSS protection)
# 2. Using * in any directive (too permissive)
# 3. Missing object-src 'none' (Flash/plugin-based attacks)
# 4. Missing base-uri 'self' (base tag injection)
# 5. Allowing 'unsafe-eval' without strict necessityKey Takeaways
- CSP is the strongest XSS defense — it tells the browser which content sources are allowed; even if an attacker injects HTML, unauthorized scripts will not execute
- Use nonces instead of
unsafe-inline— generate a unique random nonce per request and attach it to script and style tags; this allows your inline code while blocking injected code - Deploy in report-only mode first —
Content-Security-Policy-Report-Onlycollects violations without breaking anything; fix issues before switching to enforcement strict-dynamicsimplifies SPA CSP — it allows scripts loaded by already-trusted scripts, so you only need to nonce your entry point, not every dynamically loaded chunkobject-src: noneandbase-uri: selfare always required — these close plugin-based and base tag injection vectors that are often overlooked in CSP configurations


