Questions › Networking/Security
Design a Content Security Policy for a modern app. Explain nonces vs hashes, strict-dynamic, and common bypasses of weak policies.
Deep understanding of CSP as an XSS mitigation layer — how to write one that actually holds, and how weak ones get bypassed.
CSP is a response header that tells the browser which sources of script, style, images, etc. are allowed to load and execute — a defense-in-depth net that limits what an injected script can do even if XSS gets past your escaping. The naive approach, an allowlist like script-src 'self' cdn.example.com, is widely BYPASSABLE: if any allowlisted host serves a JSONP endpoint, a vulnerable AngularJS build, or user-uploaded content, an attacker abuses it; and allowlists don't stop injected inline event handlers unless you also forbid them. The modern, robust approach is a NONCE-based (or hash-based) STRICT CSP. A NONCE is a random value generated PER RESPONSE, put in the header (script-src 'nonce-r4nd0m') and echoed as an attribute on every legitimate <script nonce="r4nd0m">. The browser runs only scripts bearing the current nonce; an injected <script> from the attacker has no valid nonce (it can't predict the per-request random value, and SOP stops it reading the page), so it won't execute. The nonce MUST be cryptographically random and unique per response — a static or reused nonce is worthless. HASHES are the alternative for scripts whose content is fixed: you put the SHA-256 of the exact inline script in the policy (script-src 'sha256-...'); good for static inline blocks where a nonce is awkward. The problem nonces create: scripts loaded dynamically by your legitimate code (a script that injects another script) won't carry the nonce. 'strict-dynamic' solves this: it says 'trust scripts loaded by an already-trusted (nonced/hashed) script, and ignore host allowlists entirely'. So a solid modern policy is: script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none' — where 'unsafe-inline' and https: are IGNORED by CSP3 browsers that honor the nonce (they exist only as fallback for old browsers), object-src 'none' kills Flash/plugin vectors, and base-uri 'none' blocks <base> tag injection that could hijack relative script URLs. Additional hardening: report-uri/report-to to collect violation reports (deploy in Content-Security-Policy-Report-Only first to find breakage before enforcing), and remember CSP is a SECOND layer — it reduces impact, it doesn't replace output encoding. Common bypasses to avoid: 'unsafe-inline' without a nonce (defeats the point), 'unsafe-eval' (re-enables eval/Function), overly broad allowlists, and dangling-markup / base-uri gaps.
Writing or auditing a CSP; explaining nonces vs hashes; hardening against XSS beyond escaping.
Weak (bypassable) allowlist policy:
Content-Security-Policy: script-src 'self' cdn.example.com 'unsafe-inline'
-> JSONP/old-lib on an allowed host, or any inline, defeats it.
Strict, nonce-based policy (per response):
Content-Security-Policy:
script-src 'nonce-r4nd0mPerResponse' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
report-to csp-endpoint
<script nonce="r4nd0mPerResponse" src="/app.js"></script> <- runs
<script>stolenPayload()</script> <- no nonce -> blocked
'strict-dynamic' = trust scripts loaded by already-trusted scripts
'unsafe-inline' + https: = IGNORED by modern browsers (old-browser fallback)
Roll out with Content-Security-Policy-Report-Only first to catch breakage.
CSP is a SECOND layer — it limits XSS impact, it does not replace escaping.