Web Security

Content Security Policy: The Header That Defangs XSS

June 2, 2026 8 min read Haven Team

Cross-site scripting has been near the top of every web vulnerability list for two decades, and input sanitization alone has never fully closed it. Content Security Policy takes a different bet: assume a script will eventually slip through, and make sure the browser refuses to run it anyway.


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:

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.

The catch that ruins most policies

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.

Try Haven free for 15 days

Encrypted email and chat in one app. No credit card required.

Get Started →