surface

Learn · W3C CSP Level 3

Content-Security-Policy

The response header that limits which scripts a browser will execute.

Without a Content-Security-Policy, a browser will execute any script a page includes — a script tag you wrote, a third-party library you loaded, and, critically, anything an attacker manages to inject through a form field, a comment, a URL parameter, or any other place user input ends up rendered back into the page without being escaped. CSP is a response header that tells the browser which sources scripts, styles and other resources are actually allowed to load from, so a script that gets injected from somewhere not on that list simply refuses to run — the browser itself becomes the enforcement point, on every page, on every visit, with no code changes required at the point of injection.

The directives that matter most

  • default-src — the fallback for any resource type without its own explicit directive. 'self' is a reasonable baseline: nothing loads except from your own origin unless something more specific says otherwise.
  • script-src — specifically which sources scripts may load from. This is the directive that actually stops XSS; get this one right and the rest matters much less.
  • style-src — same idea for stylesheets. Lower stakes than script-src, but 'unsafe-inline' here still allows CSS-based data exfiltration techniques in some circumstances.
  • img-src / font-src / connect-src — where images, fonts, and fetch()/XHR/WebSocket targets are allowed to come from. connect-src is worth being deliberate about since it's what an attacker's exfiltration request would need to pass.
  • frame-ancestors — controls who may embed your page in an iframe. 'none' stops clickjacking outright; this is the modern replacement for the older X-Frame-Options header, and CSP takes precedence when both are present.
  • object-src — almost always worth setting to 'none'. Plugin content (Flash, old-style Java applets) is legacy attack surface with no legitimate use on a modern site.
  • base-uri — restricts what a <base> tag is allowed to set, closing off a specific and easy-to-overlook injection technique that can otherwise redirect every relative URL on the page.

A real policy, worked through

This is close to what Surface itself sends — a policy shaped by an actual third-party dependency (a bot-check widget) rather than a generic template:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  font-src 'self' data:;
  connect-src 'self' https://challenges.cloudflare.com;
  frame-src https://challenges.cloudflare.com;
  frame-ancestors 'none';
  base-uri 'self'

Everything defaults to same-origin only. The one external origin allowed anywhere is the bot-check widget's own domain, and it's allowed only in exactly the three directives it actually needs — script-src to load its script, frame-src to render its challenge iframe, connect-src for it to verify a response — not blanket-allowed everywhere. That's the actual discipline a CSP is supposed to enforce: each external dependency gets exactly the access it needs and nothing else, directive by directive, rather than one broad allowlist applied everywhere.

'unsafe-inline' and 'unsafe-eval' quietly remove most of the protection

Both are common, because they're the fastest way to make an existing site pass without refactoring anything — 'unsafe-inline' lets inline <script> tags and inline event handlers run, 'unsafe-eval' lets eval() and similar dynamic-code-execution APIs run. Between the two, they cover most of the ways script actually gets injected in a real XSS attack, which means a policy that includes 'unsafe-inline' in script-src is providing meaningfully less protection than the header's presence might suggest — it stops loading a remote attacker-hosted script, but does nothing to stop an injected inline <script> tag, which is the far more common XSS pattern in practice.

The correct replacement is a nonce (a random, per-response token added to both the header and the specific script tags you actually trust) or a hash (a digest of the exact inline script content, computed and added to the header). Either lets your own inline scripts run while still blocking anything injected, but both require the server to generate something per-request or per-build, which is real engineering work — which is exactly why 'unsafe-inline' remains extremely common as a starting point rather than an end state.

Rolling it out without breaking the site

Content-Security-Policy-Report-Only sends the exact same directives but doesn't block anything — it just logs what would have been blocked, either to the browser console or to a reporting endpoint named by the report-to directive (report-uri is the older equivalent, still widely supported and worth including alongside report-to for browsers that haven't caught up). Run the intended policy in report-only mode first, watch what gets flagged, fix the legitimate cases (a forgotten CDN, an inline handler that needs a nonce), and only then promote the same policy to the enforcing header. Shipping directly to an enforcing CSP without this step is the most common way a CSP rollout breaks a production site the moment it goes live.

What CSP doesn't replace

CSP is a backstop, not a substitute for actually escaping output. It exists specifically for the case where an escaping bug or an unsanitised input already let a script tag or an inline handler reach the page — at that point CSP is the last thing standing between that injected content and it actually executing. Treating CSP as the primary defence and getting lax about output encoding because "CSP will catch it" is backwards: a policy that's slightly too permissive (one leftover 'unsafe-inline', one overly broad script-src host) turns the last line of defence into no defence at all, silently, for exactly the class of bug CSP was meant to catch.

Is your domain actually covered?

Surface checks Content-Security-Policy — and everything else on this page — against your real DNS and headers, free, in seconds.

Scan your domain →