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.
- 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:
- Don’t put untrusted data in HTML/JS sinks (XSS).
- Sanitize when rich HTML is required.
- CSP to limit script capabilities.
- Cookie
HttpOnly/Secure/SameSite. - 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:
- Nonces/hashes on
<style>tags. - Refactor to classes + external CSS.
- 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-srchosts, or sandbox third parties in iframes with tightframe-src.
Reporting and rollout
- Deploy Report-Only with the target policy.
- Collect
csp-report/ Reporting API (report-to) payloads. - Fix first-party violations (inline handlers
onclick=, extension noise filter, preview envs). - Enforce on a percentage or secondary origin.
- 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.
Related on this site
- XSS for frontend engineers
- Trusted Types intro
- Same-origin policy
- iframe sandbox and security
- Permissions Policy
- Core Web Vitals — third-party tags vs strict CSP tension
Further reading
- MDN: Content Security Policy (CSP)
- web.dev: Strict CSP
- web.dev: CSP evaluator mindset
- W3C CSP Level 3
- OWASP Content Security Policy Cheat Sheet
Related guides
- XSS Prevention ChecklistActionable XSS checklist: sinks, sanitization, CSP, cookies, URLs, markdown, and PR review prompts.
- Clickjacking and X-Frame-OptionsStop UI redress attacks with frame-ancestors CSP and X-Frame-Options; know when embedding is intentional.
- Frontend Security Threat ModelThreat-model SPAs: assets, attackers, XSS/CSRF/supply-chain, and where frontend controls actually land.
- JWT Storage PitfallsWhy localStorage JWTs are XSS bait, cookie alternatives, refresh patterns, and SPA session designs that age better.
- postMessage SecuritySecure window.postMessage: targetOrigin, event.origin checks, schema validation, and iframe bridges.