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:
| Directive | Controls |
|---|---|
script-src | Where JavaScript can load from, and whether inline <script> / eval are allowed |
style-src | Inline styles and external stylesheets |
img-src | Image sources |
frame-ancestors | Who is allowed to embed this page in an iframe (the clickjacking defense) |
default-src | The 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'fromstyle-src, or removinghttps://cdn.example.comfromscript-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.