ESC

Type to search the knowledge base.

Content Security Policy (CSP)

Reduce XSS blast radius with CSP — script-src nonces/hashes, strict-dynamic, report-only rollout, and what CSP does not fix.

advanced5 min read
  • security
  • csp
  • xss
  • headers

Content Security Policy (CSP) is a browser-enforced allowlist for where scripts, styles, images, frames, and other resources may load — and whether inline script/style can run. CSP is a defense-in-depth layer against XSS, not a replacement for safe sinks and sanitization.

Docs: MDN CSP, web.dev Strict CSP, CSP Level 3, OWASP CSP Cheat Sheet.

What problem CSP solves

Attacker finds an XSS sink and injects:

<script>steal(document.cookie)</script>

With a strict CSP that forbids arbitrary inline scripts and untrusted hosts, that payload fails to execute even if it appears in the HTML. CSP does not remove the bug — the injection may still deface content or exfiltrate via allowed channels — but it cuts the worst class of script execution.

Stack defenses:

  1. Don’t put untrusted data in HTML/JS sinks (XSS).
  2. Sanitize when rich HTML is required.
  3. CSP to limit script capabilities.
  4. Cookie HttpOnly / Secure / SameSite.
  5. Trusted Types where you can.

Delivery: header vs meta

Prefer the HTTP header:

Content-Security-Policy: default-src 'self'; script-src 'nonce-rAnd0m'; style-src 'self' 'nonce-rAnd0m'

<meta http-equiv="Content-Security-Policy" content="…"> works for a subset of directives but cannot send report-to the same way as headers for all cases and is easier to get wrong on multi-page apps. Use headers (or framework middleware) in production.

Report-Only for rollout:

Content-Security-Policy-Report-Only: script-src 'nonce-…'; report-uri /csp-report

Break nothing; collect violations; tighten; then switch to enforcing.

Directives frontend engineers actually touch

Directive Controls
default-src Fallback for other fetch directives
script-src Scripts, often the XSS heart
style-src Stylesheets and often inline styles
img-src Images
connect-src fetch, XHR, WebSocket, EventSource
font-src Fonts
frame-src / child-src Frames
base-uri Restrict <base href> hijacks
object-src Plugins; set 'none'
frame-ancestors Who may embed you (clickjacking; CSPv2+ replaces X-Frame-Options for many cases)
form-action Where forms may submit
upgrade-insecure-requests Upgrade http subresources

Start from deny by default (default-src 'self' or tighter), open holes deliberately.

Inline scripts and the 'unsafe-inline' trap

A policy with script-src 'unsafe-inline' largely fails as XSS mitigation — injected inline script runs. Prefer:

Nonces

Server generates a per-request cryptographic nonce; only <script nonce="…"> matching the header runs.

Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'
<script nonce="abc123">
  window.__BOOT__ = …;
</script>
<script nonce="abc123" src="/app.js"></script>

Every response needs a new nonce. CDNs caching HTML with a fixed nonce break the model — cache HTML carefully or use hashes for static inline.

Hashes

Content-Security-Policy: script-src 'sha256-…base64…'

Hash the exact inline content. Whitespace changes invalidate the hash. Good for small static bootstraps; painful for changing markup.

'strict-dynamic'

With nonces/hashes, 'strict-dynamic' tells the browser: scripts opened by an already-trusted script may load additional scripts (parser-inserted caveats exist — read current web.dev guidance). This helps bundlers and dynamic import() while still blocking random injected tags.

Classic weak policy (don’t ship as “secure”)

Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval' data: blob:

This is theatre. unsafe-eval also enables eval / new Function — bad for XSS and some supply-chain payloads. Avoid unless a legacy dependency forces a temporary exception with a removal plan.

CSS and 'unsafe-inline'

Many design systems inject inline style="" or runtime CSS-in-JS. Options:

  1. Nonces/hashes on <style> tags.
  2. Refactor to classes + external CSS.
  3. Temporary style-src 'unsafe-inline' while you fix — script-src remains the XSS priority; still fix styles for defense depth and data exfil via CSS in exotic cases.

Framework integration notes

  • Next.js / SSR: middleware or framework CSP helpers to inject nonces into headers and React script tags. Nonce must flow from request → HTML.
  • SPAs on static hosts: pure hash-based CSP for the shell, or edge worker that adds nonces.
  • GTM / third parties: usually the reason CSP is weak. Prefer strict policy + limited script-src hosts, or sandbox third parties in iframes with tight frame-src.

Reporting and rollout

  1. Deploy Report-Only with the target policy.
  2. Collect csp-report / Reporting API (report-to) payloads.
  3. Fix first-party violations (inline handlers onclick=, extension noise filter, preview envs).
  4. Enforce on a percentage or secondary origin.
  5. Monitor breakage (login, payments, editor).

Violation reports can contain URLs — treat as sensitive logs.

What CSP does not fix

Issue Why CSP isn’t enough
HTML injection that only changes content No script needed to phish in-page
javascript: URLs in some older holes Keep URL validation
Safe DOM APIs misused with allowed origins Self-XSS via your own CDN if compromised
CSRF Use tokens / SameSite; not CSP’s job
SQL injection Server issue

CSP is necessary for mature XSS posture; it is not sufficient alone.

Minimal strict-ish starter (illustrative)

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{NONCE}' 'strict-dynamic';
  style-src 'self' 'nonce-{NONCE}';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self'

Tune img-src / connect-src to your CDNs and APIs. Add report-uri or Reporting API during rollout. Validate with MDN and browser console.

Interview angle

Define CSP as an allowlist enforced by the browser. Explain why 'unsafe-inline' guts script-src. Contrast nonce vs hash. Mention Report-Only rollout and defense-in-depth with safe sinks. Bonus: frame-ancestors vs X-Frame-Options, base-uri.

Further reading

Related guides