What is Content Security Policy?
Content Security Policy (CSP) is a security standard that provides an additional layer of defense against cross-site scripting (XSS), clickjacking, and other code injection attacks. CSP works by specifying which content sources the browser should consider valid. If an attacker manages to inject a script tag into your page, a properly configured CSP will prevent the browser from executing it.
CSP is delivered via the Content-Security-Policy HTTP response header. It consists of a series of directives, each controlling a specific type of resource. When the browser encounters a resource that violates the policy, it blocks the resource and optionally reports the violation to an endpoint you specify.
CSP Directives
Key CSP Directives
- default-src: Fallback for all resource types not explicitly configured. Start with 'self' and override as needed.
- script-src: Controls which scripts can execute. This is the most important directive for XSS prevention.
- style-src: Controls which stylesheets can be applied. Note that 'unsafe-inline' is often needed for CSS-in-JS frameworks.
- img-src: Controls which images can be loaded. Include 'data:' if you use inline images.
- connect-src: Controls which URLs can be loaded via fetch, XHR, WebSocket, and EventSource.
- frame-src: Controls which URLs can be loaded in iframes on your page.
- frame-ancestors: Controls which sites can embed YOUR page in an iframe. Replaces X-Frame-Options.
- form-action: Restricts which URLs forms can submit to. Prevents form hijacking.
- base-uri: Restricts the URLs that can be used in a base element. Prevents base tag injection.
Implementing CSP with Nonces
The most secure CSP approach for modern applications uses nonces (number-used-once). A unique, random nonce is generated for each request and added to both the CSP header and each legitimate script tag. The browser only executes scripts that have a nonce matching the one in the header.
import crypto from "crypto";
import { NextResponse, NextRequest } from "next/server";
// Next.js middleware for CSP with nonces
export function middleware(request: NextRequest) {
// Generate a unique nonce for each request
const nonce = crypto.randomBytes(16).toString("base64");
// Build the CSP header
const cspDirectives = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self' 'unsafe-inline'", // Required for Next.js
"img-src 'self' data: https: blob:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.myapp.com wss://ws.myapp.com",
"media-src 'self'",
"object-src 'none'", // Block plugins (Flash, Java)
"frame-src 'self' https://www.youtube.com",
"frame-ancestors 'none'", // Prevent framing
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
"block-all-mixed-content",
];
const cspHeader = cspDirectives.join("; ");
// Clone the request headers and add the nonce
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
// Set the CSP header on the response
response.headers.set("Content-Security-Policy", cspHeader);
return response;
}
// In your Next.js layout, use the nonce for inline scripts
// app/layout.tsx
import { headers } from "next/headers";
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} src="/analytics.js"></script>
</head>
<body>{children}</body>
</html>
);
}
CSP for Single-Page Applications
SPAs present unique CSP challenges because they often use inline scripts, dynamic script loading, and eval-based tools. The 'strict-dynamic' directive helps by allowing scripts loaded by trusted scripts to execute, even if they do not have a nonce. This is essential for applications that lazy-load code chunks.
// CSP configuration for a React/Next.js application
const cspForSPA = {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"'strict-dynamic'", // Trust scripts loaded by trusted scripts
// Hash of specific inline scripts (alternative to nonces)
"'sha256-abc123...'",
],
styleSrc: [
"'self'",
"'unsafe-inline'", // Often needed for CSS-in-JS (styled-components, emotion)
],
imgSrc: ["'self'", "data:", "https:", "blob:"],
connectSrc: [
"'self'",
"https://api.example.com",
"wss://ws.example.com",
// Add analytics, error tracking, etc.
"https://*.sentry.io",
],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"], // No plugins
mediaSrc: ["'self'"],
frameSrc: ["'self'"], // Restrict iframes
frameAncestors: ["'none'"], // Prevent being framed
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
};
// Convert to header string
function buildCSPHeader(config: typeof cspForSPA): string {
return Object.entries(config.directives)
.map(([key, values]) => {
// Convert camelCase to kebab-case
const directive = key.replace(/([A-Z])/g, "-$1").toLowerCase();
if (values.length === 0) return directive;
return `${directive} ${values.join(" ")}`;
})
.join("; ");
}
CSP Reporting
CSP violations can be reported to an endpoint you specify, allowing you to monitor policy violations in production. This is invaluable for detecting XSS attempts and for tuning your CSP policy without breaking functionality.
// CSP with reporting
const cspWithReporting = [
"default-src 'self'",
"script-src 'self' 'strict-dynamic'",
"report-uri /api/csp-report",
"report-to csp-endpoint",
].join("; ");
// Report-To header for modern browsers
const reportTo = JSON.stringify({
group: "csp-endpoint",
max_age: 86400,
endpoints: [{ url: "/api/csp-report" }],
});
res.setHeader("Content-Security-Policy", cspWithReporting);
res.setHeader("Report-To", reportTo);
// CSP report handler
app.post("/api/csp-report", express.json({ type: "application/csp-report" }), (req, res) => {
const report = req.body["csp-report"];
logger.warn({
event: "CSP_VIOLATION",
blockedUri: report["blocked-uri"],
violatedDirective: report["violated-directive"],
documentUri: report["document-uri"],
sourceFile: report["source-file"],
lineNumber: report["line-number"],
userAgent: req.headers["user-agent"],
});
res.status(204).end();
});
// Tip: Start with Content-Security-Policy-Report-Only
// This reports violations without blocking resources
// Perfect for testing a new CSP policy in production
res.setHeader("Content-Security-Policy-Report-Only", cspWithReporting);
Security Warning: CSP Mistakes
- Avoid 'unsafe-inline' for scripts: This defeats the purpose of CSP for XSS prevention. Use nonces or hashes instead.
- Avoid 'unsafe-eval': This allows eval() and similar functions, which are common XSS vectors.
- Be careful with wildcards: Using *.example.com in script-src could allow loading scripts from any subdomain, including user-controlled ones.
- Test before enforcing: Use Content-Security-Policy-Report-Only to test your policy before enforcement.
CSP Deployment Strategy
- Audit your resources: Catalog all scripts, styles, images, fonts, and API connections your app uses.
- Write an initial policy: Start restrictive and add exceptions as needed.
- Deploy as report-only: Monitor violations for at least a week in production.
- Fix violations: Update your app or policy to eliminate legitimate violations.
- Enforce the policy: Switch from report-only to enforcing mode.
- Monitor ongoing: Continue collecting reports to catch new violations.