A Pragmatic Content-Security-Policy for Static Sites and SPAs
Content-Security-Policy has a reputation for being all-or-nothing: either you do not have one, or you have one so strict it breaks your own site. The truth is friendlier. A good CSP is something you roll out gradually, measure, and tighten — and even a modest policy meaningfully reduces the damage an injected script can do. This is a pragmatic path for a static site or SPA, without the cargo-culting.
What CSP actually buys you
CSP is a browser-enforced allowlist of where content may come from. Its headline job is blunting cross-site scripting: if an attacker manages to inject a <script>, a policy that only permits scripts from your own origin (and refuses inline scripts) stops that injection from executing or phoning home. It is defense in depth — not a replacement for output encoding, but a strong second wall.
Step 1: deploy in report-only first
Never ship a brand-new enforcing policy cold; you will block your own assets. Start with Content-Security-Policy-Report-Only, which logs violations without blocking anything. Watch what would have been refused for a few days of real traffic, then fold those legitimate sources into the policy. Paste your draft into the CSP Analyzer to see how each directive is parsed and which ones are doing nothing.
Step 2: kill unsafe-inline
'unsafe-inline' on script-src defeats most of the point of CSP — it re-allows exactly the inline scripts an XSS would inject. Two ways to remove it:
- Hashes — for a fixed set of inline scripts, add their SHA-256 to the policy (
'sha256-...'). The browser runs only inline scripts whose hash matches. Compute those hashes with the Hash Generator. - Nonces — for server-rendered pages, emit a fresh random
nonceper response and tag your legitimate scripts with it.
Static sites usually favor hashes; SPAs with a server layer often use nonces. Either way, the goal is the same: the browser executes inline script only if you vouched for it.
Step 3: lock down the obvious directives
A reasonable starting policy for a static site:
default-src 'self';
script-src 'self' 'sha256-<your-inline-hashes>';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://api.yourservice.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
connect-src is the one people forget — it governs where fetch/XHR/WebSocket may go, which is precisely the channel a compromised script would use to exfiltrate data. Pin it to the APIs you actually call. frame-ancestors 'none' blocks clickjacking; base-uri 'self' stops <base> hijacks. When you inspect third-party URLs to decide what belongs in connect-src, the URL Parser makes the host and path explicit.
Step 4: measure, then enforce
Once report-only has been quiet for real traffic, flip the header to enforcing. Keep an eye on reports after the switch and treat new violations as either a real bug to fix or a missing source to add. The CSP Header Checklist covers the full report-only-to-enforcement path; the broader Web Security Headers Guide puts CSP alongside the other headers worth setting.
The takeaway
A CSP does not have to be perfect to be valuable. Start report-only, remove unsafe-inline with hashes or nonces, pin connect-src, and enforce once it is quiet. A site that ships even a moderate policy has already taken the cheapest, highest-leverage step against the script it never meant to run.
Sources
- MDN — Content-Security-Policy — https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
- W3C Content Security Policy Level 3 — https://www.w3.org/TR/CSP3/