Content Security Policy from scratch

8 min read
security
http

Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of scripts, styles, images and other resources are allowed to load on a page. Its main practical benefit is cutting off the impact of a successful XSS injection — even if an attacker manages to insert a <script> tag, a correctly configured CSP can stop the browser from executing it.

The basic shape

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none';

Each directive controls a specific category of resource. default-src is the fallback for any directive you don't specify explicitly, but it's worth setting most categories explicitly rather than relying on the fallback, since it's easy to forget which categories inherit from it.

The directives that matter most

  • script-src — the single most important directive for XSS mitigation. Restrict it to your own origin and any script CDNs you actually use; avoid 'unsafe-inline' here if at all possible, since it defeats most of the point (an injected inline <script> becomes allowed too).
  • style-src — similar idea for stylesheets; less security-critical than script-src, since CSS injection alone is a much weaker attack vector, but still worth restricting.
  • img-src, font-src, media-src — usually just about limiting where assets load from, not primarily an XSS defense.
  • connect-src — controls where fetch, XMLHttpRequest, WebSocket and EventSource connections can go. Missing this is a common way exfiltration via injected code still succeeds even when script-src looks locked down, because the injected code was allowed to run under a permissive script-src but the policy didn't block where it could send data.
  • frame-ancestors — replaces the old X-Frame-Options header for controlling who can embed your page in an <iframe>; 'none' is the strongest anti-clickjacking setting.
  • object-src 'none' — Flash/plugin content is a legacy attack surface; disabling it entirely is safe on virtually every modern site.

'unsafe-inline' and 'unsafe-eval' are named honestly

'unsafe-inline' allows inline <script> blocks and onclick=-style handlers to execute; 'unsafe-eval' allows eval(), new Function(), and similar dynamic code execution. Both directly undermine CSP's main value against XSS, because they're exactly the mechanisms an injected payload would use. If your codebase relies on inline scripts, the better fix is to move them into external files or use nonces/hashes (below), not to blanket-allow inline execution.

Nonces and hashes: inline scripts without 'unsafe-inline'

If you genuinely need an inline <script> block (some server-rendered apps do, for a bootstrap payload), CSP lets you allow it specifically rather than allowing all inline scripts:

Content-Security-Policy: script-src 'self' 'nonce-r4nd0mBase64Value';
<script nonce="r4nd0mBase64Value">
  window.__INITIAL_STATE__ = { /* ... */ };
</script>

The nonce must be a fresh, unpredictable value generated per response (not hardcoded), or an attacker who can inject a <script nonce="..."> tag with a guessed or leaked nonce defeats the protection. A hash-based alternative ('sha256-...') works similarly but requires the exact script content to match a precomputed hash, which is more brittle if the inline content changes.

Roll it out with report-only mode first

Deploying a strict CSP straight to production risk breaking legitimate functionality you didn't account for (a forgotten CDN, a third-party widget). Content-Security-Policy-Report-Only lets the browser evaluate the policy and send violation reports without actually blocking anything:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports;

Run in report-only mode, collect violations for a realistic period covering your traffic patterns, tighten the policy based on what legitimately needs to be allowed, then switch to the enforcing header.

Common mistakes

  • Setting default-src * "to be safe," which allows nearly everything and provides close to no protection.
  • Adding 'unsafe-inline' to make a console warning go away without understanding what it disables.
  • Forgetting connect-src, leaving a route open for data exfiltration even when script sources look locked down.
  • Not testing report-only first, and instead discovering the policy blocked payment or analytics scripts in production.

Building one

Rather than hand-writing directives and hoping the syntax is right, the CSP builder generates a policy from a checklist of what your page actually needs, and the security headers auditor checks a live URL's response headers, including CSP, against current best practice.

Tools from this article

Comments

No login needed. Comments appear after a quick review.

Protected by an on-site captcha — no third-party trackers.

Optional: get an alert when your comment is published or replied to, plus new tool announcements. No sign-up.

Loading comments…

← All articles