Skip to content
Advanced13 min study

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.

Question progress0 / 10 completed
Start the lesson
Content Security Policy and security headers visual explanation

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.

Think of it like this: they're like the building's own security rules posted at the front desk and enforced by the browser as the guard: no outside contractors' equipment runs here (CSP script-src), this office can't be shown inside another company's lobby (frame-ancestors), don't tell visitors which floor you came from (Referrer-Policy).

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

HTTP
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-origin

Scripts 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

How to say it out loud: "Security headers are enforced by the browser on every response, so they cover things the app can't guarantee about itself. CSP is the big one — it's not an XSS fix, output encoding is, but a strong CSP means that even if an injection sink slips through, the injected script has no nonce and doesn't run, so the bug goes from critical to low impact. I'd build the policy in report-only mode against real traffic, use per-request nonces with strict-dynamic instead of a host allowlist since allowlists are trivially bypassed, and pair it with HSTS, frame-ancestors to block clickjacking, and a tight Referrer-Policy."

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?

  1. Confirm whether a CSP is deployed and what script-src allows.
  2. Deploy or tighten a nonce-based CSP without unsafe-inline.
  3. Add strict-dynamic so first-party bundles still load their dependencies.
  4. Set frame-ancestors and a strict Referrer-Policy as further containment.
  5. 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.

Verify and go deeper