Content Security Policy
Encoding (Chapter 6) and sanitization (Chapter 7) prevent injection. Content Security Policy is different in kind: it's a browser-enforced safety net that limits the damage if a payload does slip through. It's the classic defence-in-depth layer for XSS — not a substitute for encoding, but a powerful backstop that can turn a critical XSS into a non-event.
What CSP Does
CSP is an HTTP response header (Content-Security-Policy) that tells the browser which sources of content are allowed to load and execute. The browser enforces it. For XSS, the decisive power is controlling what scripts may run: even if an attacker injects <script> into the page, a good CSP means the browser refuses to execute it.
This says: load resources only from our own origin, run scripts only from our own origin (not inline, not from other domains), and block plugins entirely. An injected inline <script>alert(1)</script> violates script-src 'self' and is not executed.
Key Directives
| Directive | Controls |
|---|---|
| default-src | fallback for all resource types not otherwise specified |
| script-src | where scripts may load/execute — the key XSS control |
| style-src | where stylesheets may come from |
| img-src / font-src / connect-src | images / fonts / fetch/XHR/WebSocket targets |
| object-src | plugins (<object>/<embed>) — set to 'none' |
| base-uri | restricts <base> — set to 'self' to block base-tag hijacks |
| frame-ancestors | who may frame you (clickjacking defence) |
Common source values: 'self' (same origin), 'none' (nothing), a domain allowlist, 'nonce-…', 'sha256-…', and the dangerous 'unsafe-inline' / 'unsafe-eval'.
The Hard Part: Inline Scripts
A strict script-src 'self' blocks all inline scripts — including the legitimate ones most sites have (<script>…</script> in the page, onclick= handlers). That's what makes CSP effective against XSS (injected scripts are inline), but it's also why adopting CSP is work. There are two safe ways to allow your inline scripts while still blocking injected ones: nonces and hashes.
The nonce is unpredictable and regenerated per response, so an attacker injecting a script can't know it and can't add a valid nonce= attribute — their script is blocked while yours runs. Hashes work similarly for static inline scripts: you put the script's SHA-256 in the policy ('sha256-…'), and only scripts matching that exact content execute.
'unsafe-inline' to script-src — which re-allows all inline scripts, including the attacker's. A policy of script-src 'self' 'unsafe-inline' provides essentially zero XSS protection, because injected inline scripts are exactly what it now permits. Likewise 'unsafe-eval' re-enables eval()-class sinks. A CSP that lists 'unsafe-inline' for scripts looks like a defence but isn't one. Use nonces or hashes instead — that's the entire point of CSP for XSS.
Why CSP Is Defence-in-Depth, Not the Primary Fix
CSP must not be your only XSS defence, for several reasons: policies are easy to misconfigure (one 'unsafe-inline' and it's hollow); some XSS doesn't need to inject script tags (e.g. stealing data via allowed image/connect requests, or DOM-clobbering); older browsers have partial support; and a too-strict policy that breaks the site pressures teams to weaken it. Encoding and sanitization stop the injection; CSP limits the blast radius if they fail. Together they're strong; CSP alone is a leaky net.
Content-Security-Policy-Report-Only first — the browser reports violations (to a report-uri/report-to endpoint) without blocking anything. You collect real violation data, fix your inline scripts (move to nonces/external files), tighten the policy until reports are clean, then switch to the enforcing header. This avoids the "strict CSP broke prod, revert it" cycle that leads to 'unsafe-inline'.
A Strong Starting Policy
Modern guidance favours a nonce-based, "strict-dynamic" policy over long domain allowlists (which are easy to bypass via an allowlisted host's open redirect or a vulnerable script on it). Tools like Google's CSP Evaluator grade a policy and flag weaknesses like 'unsafe-inline' or overly broad allowlists.
Hands-On Exercises
Given Content-Security-Policy: script-src 'self', explain what happens to (a) a legitimate external script from your own origin, (b) an injected inline <script>, (c) an injected <img onerror> handler. State why CSP turns an injected script into a non-event.
Explain how a CSP nonce lets your inline scripts run while blocking an attacker's injected inline script. Why must the nonce be random and per-response? Then explain why adding 'unsafe-inline' to make the site work destroys the protection.
Argue why CSP should be defence-in-depth rather than the primary XSS defence. Give two ways a misconfigured or bypassed CSP still leaves XSS exploitable, and describe the report-only rollout strategy and why it prevents the "strict CSP broke the site" failure mode.
📄 View solutionChapter 8 Quick Reference
- CSP = a response header telling the browser which content sources may load/execute — a browser-enforced safety net
- Key XSS control is script-src: a good policy means an injected
<script>simply won't run - Useful directives:
default-src,script-src,object-src 'none',base-uri 'self',frame-ancestors - Nonces (random, per-response) and hashes let your inline scripts run while blocking injected ones
- 'unsafe-inline' destroys CSP's XSS value — it re-allows the attacker's inline scripts; the #1 mistake (also avoid
'unsafe-eval') - CSP is defence-in-depth, not primary — encoding/sanitization stop injection; CSP limits the blast radius if they fail
- Roll out with Content-Security-Policy-Report-Only first, fix violations, then enforce; prefer nonce +
strict-dynamicover domain allowlists - Grade policies with CSP Evaluator; watch for
'unsafe-inline'and broad allowlists - Next chapter: framework protections & modern XSS — React/Vue/Angular auto-escaping, escape hatches, Trusted Types