Deep DivesContent Security Policy

Content Security Policy (CSP)

One-line relationship: CSP is the browser-enforced allowlist for what a page is permitted to load and run — the backstop for when XSS escaping fails, and the mechanism behind clickjacking’s frame-ancestors directive.

What it is

CSP is a response header (Content-Security-Policy: ...) built from directives, each controlling one category of resource:

DirectiveControls
script-srcWhere JavaScript can load from, and whether inline <script> / eval are allowed
style-srcInline styles and external stylesheets
img-srcImage sources
frame-ancestorsWho is allowed to embed this page in an iframe (the clickjacking defense)
default-srcThe fallback for any directive not explicitly listed

Sources are matched against a value like 'self' (same origin), an explicit host (https://cdn.example.com), a scheme (data:), or dangerous escape hatches like 'unsafe-inline' and 'unsafe-eval' that most of a strict policy’s value comes from not including.

Try it — parse a policy

Content-Security-Policy header

Would this load / run?Result
Inline <script>...</script>
needs 'unsafe-inline' (or a matching nonce/hash) in script-src
Blocked 🛡
eval() / new Function()
needs 'unsafe-eval' in script-src
Blocked 🛡
External <script src="https://cdn.example.com/lib.js">
needs the host listed in script-src (or default-src)
Allowed ⚠
Inline style="..." / <style>
needs 'unsafe-inline' in style-src
Allowed ⚠
External <img src="https://images.example.com/logo.png">
needs the host listed in img-src (or default-src)
Allowed ⚠
The lesson: this is a pure client-side parse of the policy string — no real enforcement, just the same directive-matching logic the browser applies. Try deleting 'unsafe-inline' from style-src, or removing https://cdn.example.com from script-src, and watch results flip. A tight CSP (no 'unsafe-inline', no 'unsafe-eval', an explicit allowlist) is the backstop that makes a successful XSS injection inert — the attacker's script tag can exist in the DOM, but the browser refuses to execute it.

Why it’s the XSS backstop

Output encoding should stop every injection before it reaches the DOM — but “should” is doing a lot of work across a large codebase, third-party components, and template engines with an escape hatch. CSP doesn’t try to detect or sanitize an injection; it simply refuses to execute inline scripts or load from unlisted origins, full stop. An attacker who successfully injects <script> into the page still fails, because the browser won’t run it without 'unsafe-inline' (or a matching nonce/hash) in script-src. This is why the CSP directive-loosening review question — “why does this need 'unsafe-inline'?” — matters more than it looks like it should.

What to take to the client

A CSP audit is cheap and the payoff is asymmetric: most of the value comes from removing two tokens ('unsafe-inline', 'unsafe-eval') that a lot of legacy code depends on by habit rather than necessity. frame-ancestors alone is worth shipping even before the rest of the policy is tightened — it’s the entire clickjacking defense in one line.