Content Security Policy
A Content Security Policy is the HTTP header that tells the browser which sources of script, style, fonts, frames, and connect targets are allowed. The strongest single XSS defence after output encoding.
Strict CSP with nonces + reporting
EXAMPLE
// 1) Express middleware — set CSP on every response
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self' 'nonce-" + res.locals.cspNonce + "' 'strict-dynamic'",
"style-src 'self' 'nonce-" + res.locals.cspNonce + "'",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests",
"report-to csp-endpoint",
].join('; '));
next();
});
// 2) Templates render scripts with the nonce
// <script nonce="<%= cspNonce %>">…inline app code…</script>
// <link rel="stylesheet" nonce="<%= cspNonce %>" href="/css/app.css">
// 3) Set up a reporting endpoint
app.use(express.json({ type: ['application/csp-report', 'application/reports+json'] }));
app.post('/csp-report', (req, res) => {
log.warn({ csp: req.body }, 'csp violation');
res.status(204).end();
});
res.setHeader('Reporting-Endpoints', 'csp-endpoint="/csp-report"');
# 4) Roll out with report-only first — see what breaks
res.setHeader('Content-Security-Policy-Report-Only', '...same policy...');
# Watch /csp-report for a week, adjust, then switch to enforcing.
# 5) Common policies — pick by app shape
# A) Pure SSR + small inline scripts
# script-src 'self' 'nonce-xyz' 'strict-dynamic'
#
# B) SPA — all scripts are bundled
# script-src 'self'
# style-src 'self' 'sha256-...'
#
# C) Allow specific CDN (try to avoid)
# script-src 'self' https://cdn.example.com
# 6) Things CSP blocks
# • Inline scripts WITHOUT nonce / hash → blocked → XSS dies
# • Inline event handlers (onclick="...") → blocked unless 'unsafe-inline'
# • eval() → blocked unless 'unsafe-eval'
# • Loading from unlisted sources → blocked → exfil dies
# 7) Things to AVOID
# • 'unsafe-inline' in script-src → defeats CSP for scripts
# • 'unsafe-eval' → enables many lib + framework attacks
# • Wildcards in script-src / connect-src → enables exfil
# • Static nonces → nonce must be per-response
# 8) Frameworks that need 'strict-dynamic'
# 'strict-dynamic' means "if a script with my nonce loaded this, trust it".
# Lets your bundler emit chunks without re-listing each one.
# 9) Pair with other headers — defence in depth
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
res.setHeader('Strict-Transport-Security','max-age=31536000; includeSubDomains; preload');
# 10) Tools
# • https://csp-evaluator.withgoogle.com — paste your policy, get a critique
# • Browser DevTools → Network → look at the response header
# • Mozilla Observatory — overall security headers grade
Why it matters
A strict CSP with nonce + strict-dynamic kills almost every reflected and stored XSS in a modern app — even if your output encoding missed a spot, the injected script can’t execute without your nonce.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Reduces XSS impact by restricting what scripts can run.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
require-trusted-types-for 'script';
Try it Yourself »
Exercise
Header name to set a Content Security Policy.
: default-src 'self'
Hyphenated.
Discussion
Loading…