Most web defenses try to stop bad input from ever reaching the page. Content Security Policy (CSP) accepts that, on a large enough application, something eventually will, and asks a second question instead: even if attacker-controlled markup lands in this page, should the browser execute it? A good CSP answers "no" — and because the browser enforces the rule, the answer doesn't depend on a developer remembering to escape one more string.
CSP is delivered as an HTTP response header, Content-Security-Policy, sent with the page. Its value is a list of directives, each naming a resource type and the sources the browser may load that type from. The browser parses the policy once and applies it to everything the page subsequently tries to do.
Reading a Policy
A policy is a sequence of directives separated by semicolons. Each directive is a name followed by a space-separated source list:
Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' data:; frame-ancestors 'none'
Read aloud, that says: by default, load resources only from this same origin; scripts only from this origin; images from this origin plus inline data: URIs; and let no one embed this page in a frame. The directives you'll meet most often:
- default-src — the fallback source list for any resource type you didn't name explicitly.
- script-src — the most security-critical directive; controls which JavaScript may execute.
- style-src, img-src, font-src, connect-src — control stylesheets, images, fonts, and the destinations
fetch/XHR/WebSocket may reach. - frame-ancestors — who may embed this page in an iframe; the modern replacement for the
X-Frame-Optionsheader against clickjacking.
Common source keywords include 'self' (the page's own origin), 'none' (nothing), an explicit origin like https://cdn.example.com, and the two dangerous escape hatches: 'unsafe-inline' and 'unsafe-eval'.
Why Inline Scripts Are the Whole Game
Classic reflected and stored XSS works by getting markup like <script>steal()</script> or an onerror= handler into the page. These are inline scripts — code that lives in the HTML itself rather than in a separate file. The single most valuable thing a CSP does is refuse to execute inline scripts.
A script-src that lists only trusted origins and omits 'unsafe-inline' means an injected <script> tag simply does not run. The browser sees code that wasn't authorized by the policy and blocks it. The injection still happened — but it's inert.
Adding 'unsafe-inline' to script-src to make a legacy inline handler work re-permits exactly the scripts CSP exists to block. A policy with 'unsafe-inline' in its script directive provides almost no XSS protection. It is the difference between a lock and a lock left open.
Nonces, Hashes, and Strict CSP
Real applications often need some inline script. CSP offers two ways to allow specific inline scripts without opening the door to all of them.
Nonces
The server generates a fresh random value for each response, puts it in the policy as script-src 'nonce-r4nd0m', and tags its own legitimate scripts with the matching <script nonce="r4nd0m">. The attacker, injecting markup after the fact, can't guess the per-request nonce, so their script lacks the token and is blocked. The nonce must be unpredictable and regenerated every response — a static nonce is no nonce at all.
Hashes
Alternatively, the policy can list the cryptographic hash of an allowed inline script, e.g. 'sha256-…'. The browser hashes each inline block and runs only those whose digest is on the list. This suits static scripts that don't change between requests.
Modern guidance — what's often called strict CSP — combines nonces with the 'strict-dynamic' keyword, which says "trust scripts loaded by an already-trusted script," letting you drop fragile host allowlists entirely. Allowlist-based policies have a long history of being bypassed through a single permitted CDN that also hosts an exploitable library or an open redirect, which is why nonce-plus-'strict-dynamic' has become the recommended shape.
Telling the Browser to Report Back
CSP can run in two modes. Content-Security-Policy enforces — violations are blocked. Content-Security-Policy-Report-Only blocks nothing but sends a JSON report to an endpoint you specify, which is invaluable for rolling out a policy without breaking a live site. In either mode, the report-uri (or newer report-to) directive names where the browser POSTs a structured description of each violation.
Those reports double as an intrusion signal. A sudden spike of script-src violations from real users can be the first sign that someone is actively trying to inject code into your pages — a free tripwire that comes with the policy.
What CSP Does Not Do
CSP is a mitigation, not an absolution. It's worth being clear about its edges:
| CSP helps with | CSP does not replace |
|---|---|
| Blocking injected and inline scripts | Server-side input validation and output encoding |
| Restricting where data can be exfiltrated (connect-src) | Fixing the injection flaw itself |
| Clickjacking defense (frame-ancestors) | CSRF tokens and SameSite cookies |
| Forcing HTTPS subresources (upgrade-insecure-requests) | Transport security policy (HSTS) |
An over-permissive policy gives a false sense of safety; a too-strict one breaks the site and gets hastily loosened back into uselessness. The healthy path is to deploy in report-only mode, watch the violation stream from real traffic, tighten until legitimate scripts are all accounted for by nonces or hashes, and only then enforce. Pair it with Subresource Integrity so that even an allowed third-party script can't be silently swapped for a malicious version.
Where Haven Fits
Haven's web app ships a deliberately tight policy. Its script sources are restricted to the app's own origin with no third-party script origins and no 'unsafe-inline'; remote email images are never loaded directly into the page but routed through a server-side image proxy, so a tracking pixel can't phone home from the page's own context; and every page carries a reporting directive so violations surface rather than disappear. None of that is exotic — it's the boring, well-understood shape of a strict CSP, applied consistently.
That's the real takeaway. CSP isn't clever, and it isn't new. It's a second wall behind your input handling, enforced by the browser instead of by hope — and the applications that resist XSS in practice are the ones that bothered to build it correctly.