Content Security Policy and security headers
Use browser-enforced response headers as a defense-in-depth layer that limits XSS blast radius, framing, and API exposure.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain content security policy and security headers in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Explain it simply
Security-related HTTP response headers tell the browser to enforce restrictions the page can't guarantee itself — which scripts may run, whether the site may be framed, how much referrer info to leak, which powerful APIs are allowed. They're a defense-in-depth layer set by the server.
One-line definition: Use browser-enforced response headers as a defense-in-depth layer that limits XSS blast radius, framing, and API exposure.
02Mental model
The browser applies these headers to every response before the page runs. Content-Security-Policy is the most involved: it allowlists sources per resource type, and a strong policy uses per-request nonces or hashes plus strict-dynamic rather than URL allowlists, which are easy to bypass. Others are simple switches — HSTS forces HTTPS, frame-ancestors stops clickjacking, Referrer-Policy limits URL leakage, Permissions-Policy disables unused APIs.
03Step by step
- Start CSP in Content-Security-Policy-Report-Only mode and collect violation reports from real traffic.
- Move inline scripts to files or give them a per-response nonce; avoid unsafe-inline and broad host allowlists.
- Add strict-dynamic so trusted scripts can load their own dependencies without listing every CDN.
- Set HSTS, frame-ancestors 'none' or an allowlist, and a restrictive Referrer-Policy.
- Use Permissions-Policy to turn off APIs the site doesn't use.
- Enforce the policy only after report-only has been clean long enough to trust it.
04Working example
Content-Security-Policy: default-src 'self'; script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'Strict-Transport-Security: max-age=31536000; includeSubDomainsReferrer-Policy: strict-origin-when-cross-originScripts run only if they carry the matching per-response nonce, and strict-dynamic lets those trusted scripts load further scripts without a host allowlist — which defeats most injected-script attacks even if an XSS sink exists. The other headers force HTTPS, forbid framing, and trim referrer data.
05Where it is used
- Reducing the impact of an XSS bug from runs any script to runs almost nothing
- Stopping clickjacking without JavaScript frame-busting
- Preventing leakage of full URLs, which may contain tokens, to third parties
- Disabling browser APIs an app never uses so a compromise can't reach them
06Common mistakes
- Shipping CSP with unsafe-inline in script-src, which allows exactly the injected inline scripts CSP is meant to block
- Relying on a host allowlist — a single vulnerable path or JSONP endpoint on an allowed host bypasses it
- Enforcing a brand-new policy directly in production without a report-only shakeout, breaking legitimate scripts
- Treating headers as a substitute for output encoding rather than a second layer behind it
07Interview answer
Frame CSP as defense-in-depth that reduces XSS blast radius, not a primary XSS fix. Know that nonce plus strict-dynamic beats host allowlists, and that report-only mode is how you deploy it safely.
A site sets script-src 'self' 'unsafe-inline'. Why does this CSP provide almost no XSS protection?
unsafe-inline permits any inline script, which is exactly what a reflected or stored XSS payload injects, so the policy allows the attack it was added to prevent; a nonce or hash based policy without unsafe-inline is what actually blocks injected inline scripts.
DDConcept deep dives
Deep dive 1
Security headers are browser-enforced, and independent of app code
Headers like Content-Security-Policy, Strict-Transport-Security, and Permissions-Policy are instructions the browser applies to every response before the page runs, so they hold even if application code has a bug. That's what makes them defense-in-depth: output encoding prevents XSS, and CSP is the layer that limits the damage when encoding is missed somewhere.
- The server, not the app bundle, is responsible for setting these.
- A header is applied earlier and more reliably than an equivalent meta tag, and some directives require a real header.
- They reduce blast radius; they are not a substitute for the primary fix.
Deep dive 2
A strong CSP is nonce-based, not allowlist-based
Host allowlists (script-src https://cdn.example.com) trust every script on an allowed origin, and one JSONP endpoint, open redirect, or vulnerable path there can be turned into arbitrary script execution. A per-response nonce trusts only the specific scripts the server stamped for that response, which an injected script can't predict, and 'strict-dynamic' then lets those trusted scripts load their own dependencies without any host rules.
- 'unsafe-inline' in script-src defeats the entire policy — it permits exactly what injection produces.
- nonce plus strict-dynamic is the current recommended shape for a script CSP.
- Deploy with Content-Security-Policy-Report-Only first and fix violations before enforcing.
Deep dive 3
The other headers each close a specific, narrow gap
Beyond CSP, each security header addresses one concrete risk: HSTS stops protocol downgrade on the network, frame-ancestors (or legacy X-Frame-Options) stops clickjacking via embedding, Referrer-Policy stops leaking full URLs and their tokens to third parties, and Permissions-Policy denies powerful APIs the site never uses so a compromise can't reach them. None is complex individually; the value is applying the whole set.
- Clickjacking protection is a header, not JavaScript frame-busting.
- A strict Referrer-Policy prevents query-string tokens from leaking in the Referer header.
- Turn off camera, microphone, and geolocation via Permissions-Policy if the app doesn't use them.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Conceptual · 1 min · Question 1Is Content Security Policy a fix for XSS?Open model answer
Model answer
No. Output encoding and safe rendering APIs prevent XSS; CSP is defense-in-depth that limits the damage if an injection sink is missed. A strong CSP can reduce a critical XSS to negligible impact by ensuring the injected script simply doesn't execute.
Open question page →Advanced · Conceptual · 1 min · Question 2Why is a nonce-based CSP stronger than a host allowlist?Open model answer
Model answer
A host allowlist trusts every script served from listed origins, and a single vulnerable endpoint, open redirect, or JSONP callback on an allowed host can be abused to run attacker-controlled code. A per-response nonce only trusts scripts the server explicitly stamped for that response, which an injected script can't guess.
Open question page →Advanced · Conceptual · 1 min · Question 3What does 'strict-dynamic' do in a CSP?Open model answer
Model answer
It propagates trust from a script that already passed the nonce or hash check to scripts that script loads dynamically, so trusted code can pull in its own dependencies without every CDN being allowlisted. It effectively replaces host-based rules with propagated trust.
Open question page →Intermediate · Conceptual · 1 min · Question 4How do you deploy a CSP without breaking the site?Open model answer
Model answer
Ship it first as Content-Security-Policy-Report-Only, which reports violations without blocking, collect reports from real traffic, fix legitimate scripts flagged as violations, and only switch to the enforcing header once reports are clean.
Open question page →Intermediate · Conceptual · 1 min · Question 5What does Strict-Transport-Security (HSTS) protect against?Open model answer
Model answer
It tells the browser to only ever connect to the origin over HTTPS for a set duration, preventing an attacker on the network from downgrading the first or a subsequent request to HTTP and intercepting it. The preload list extends this to the very first visit.
Open question page →Intermediate · Conceptual · 1 min · Question 6How do X-Frame-Options and CSP frame-ancestors relate?Open model answer
Model answer
Both control whether the page can be embedded in a frame, which is the primary clickjacking defense. frame-ancestors is the modern, more expressive mechanism (it supports multiple origins and is part of CSP); X-Frame-Options is the older header kept for legacy browser support.
Open question page →Intermediate · Conceptual · 1 min · Question 7What is Referrer-Policy for?Open model answer
Model answer
It controls how much of the current URL is sent in the Referer header on outgoing requests and navigations. A strict value like strict-origin-when-cross-origin prevents leaking full paths and query strings — which can contain tokens or identifiers — to third-party origins.
Open question page →Intermediate · Conceptual · 1 min · Question 8What does Permissions-Policy control?Open model answer
Model answer
It allows or denies access to powerful browser features — camera, microphone, geolocation, and others — per origin and per frame. Disabling features the site never uses means a compromise or a misbehaving third-party frame can't invoke them.
Open question page →Advanced · Conceptual · 1 min · Question 9Can a CSP be set with a meta tag instead of a header?Open model answer
Model answer
Most directives can, which is useful when you don't control response headers, but some — notably frame-ancestors, sandbox, and report-uri — only work as a real header. A header is also applied earlier, before any markup is parsed.
Open question page →Intermediate · Conceptual · 1 min · Question 10What's the value of CSP violation reporting in production?Open model answer
Model answer
Reports reveal injection attempts, misconfigured third-party scripts, and browser-extension interference in real time, giving both a security signal and a maintenance signal. report-to or the older report-uri directive sends structured reports to an endpoint you monitor.
Open question page →SCScenario questions
Scenario 1
A security review finds a stored XSS vulnerability in a comment field. The fix will take a sprint. What can be done immediately to reduce risk?
- Confirm whether a CSP is deployed and what script-src allows.
- Deploy or tighten a nonce-based CSP without unsafe-inline.
- Add strict-dynamic so first-party bundles still load their dependencies.
- Set frame-ancestors and a strict Referrer-Policy as further containment.
- Still prioritize the actual output-encoding fix.
Reveal worked answer
The real fix is encoding the comment output, but as immediate mitigation I'd deploy or harden a CSP: a per-response nonce on script-src with no unsafe-inline, plus strict-dynamic so the app's own bundles keep working. With that in place, the stored payload has no valid nonce and the browser refuses to execute it, so the vulnerability's impact drops from account takeover to essentially nothing while the proper fix ships. I'd roll it out via report-only first if there's any doubt about breaking legitimate scripts.