Content Security Policy — From Zero to Working CSP

CSP is how you tell the browser which origins are allowed to execute or render subresources for a document. Done well, it shrinks your XSS blast radius. Done lazily—think unsafe-inline everywhere—it’s theater. Below: why CSP exists, then how to build a policy from what your page actually loads.

Why CSP exists

Server-side escaping helps, but templates drift, admin panels grow inline scripts, and marketing drops tags you never reviewed. CSP adds a second gate: even if someone injects markup, the browser refuses to run or load what the policy forbids. That is why auditors ask for it beside HttpOnly cookies and framework defaults.

Directive tour

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https://cdn.example; connect-src 'self' https://api.example; report-uri /csp-report;

Generate from real resources

Export your production HTML, list every <script src>, stylesheet, preload, font face, and connect domain. Group them by directive, drop duplicates, then add temporary 'unsafe-inline' only where you still rely on inline bootstraps. CSPFixer automates that inventory from a URL—less fun than grep, fewer misses than memory.

Moving off unsafe-inline

Hashes work for static inline blobs:

script-src 'self' 'sha256-abc123...'

Nonces scale better when HTML is rendered per request:

Content-Security-Policy: script-src 'self' 'nonce-rAnDoM'
<script nonce="rAnDoM">initCharts();</script>

The nonce must be unpredictable per response and mirrored exactly on each allowed inline block. If your framework caches HTML without varying the nonce, violations explode.

Rollout that does not wake you at 3am

Run Report-Only in production for traffic you trust, watch reports for unexpected hosts, then promote to enforcing CSP on a subset of routes. Pair with integration tests that hit real pages—Lighthouse and manual clicks catch what logs miss.

Third parties and worker scripts

Analytics and A/B snippets often load from domains you do not control long term. Rather than widening script-src to entire TLDs, proxy known tags through your domain or pin specific file paths when vendors version URLs. Service workers count as same-origin script context: if you register a worker, ensure script-src includes its origin and that importScripts targets are enumerated.

When inline JSON-LD is allowed

Structured-data blocks in <script type="application/ld+json"> execute as script in CSP terms. Either hash the stable JSON blob or allow the inline block via nonce on that element. Teams frequently forget those blocks when tightening policy and then watch rich-result validators fail silently in headless browsers.

Meta vs header CSP

Browsers honor CSP delivered as an HTTP header first; a <meta http-equiv> tag only applies to the document itself and cannot cover worker scripts or enforce on subresources as cleanly. Prefer header-based policy at your CDN or origin so every HTML response carries the same contract, and keep meta tags for legacy marketing pages only when you cannot touch the edge.

Open CSPFixer →