Published
Writing a Content Security Policy that does not break the site
Content Security Policy is a response header that tells the browser where scripts, styles, images and everything else are allowed to load from. Done well it makes most injected-script attacks fail outright, because the injected script is not from an allowed origin. Done badly it blanks the page, which is why so many sites either do not have one or have one that says 'unsafe-inline' everywhere and protects nothing.
What the header looks like
A policy is a list of directives, each naming a resource type and a list of allowed sources:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'
default-src is the fallback for anything not named. 'self' means the page's own origin. Everything else is an explicit host or a keyword. The CSP builder generates the header from checkboxes and host lists, which is far less error-prone than writing it by hand, because the quoting rules (keywords in single quotes, hosts without) trip up everyone at least once.
Start in report-only mode
The reason a CSP breaks a site is that nobody knows everything the site loads. There is always a tag manager that injects a script, a font from a CDN nobody remembers adding, a widget that inlines a style. So do not deploy the enforcing header first. Deploy the same policy as Content-Security-Policy-Report-Only with a report-uri or report-to directive, and the browser will log every violation without blocking anything.
Leave that running for a week across real traffic. The reports tell you exactly which origins the site needs. Add them, redeploy, repeat until the reports go quiet, then switch the header name to the enforcing one.
Inline scripts and styles
The directive people give up on is script-src, because almost every site has inline <script> blocks and 'unsafe-inline' allows them, at the cost of allowing injected inline scripts too. There are two better options. A nonce is a random value generated per response, put in the header ('nonce-abc123') and on each legitimate script tag; the browser runs only tags that carry it. A hash is the SHA-256 of a specific inline script's contents, listed in the header; this suits static sites where the inline blocks never change. The hash generator produces the SHA-256 for a block of script text, which you then encode and add as 'sha256-...'.
Inline styles are lower risk and 'unsafe-inline' on style-src is a pragmatic compromise for most sites, though the same nonce and hash options exist.
Test before you ship
Once you have a candidate header, the CSP tester parses it and reports the problems: directives that are missing, sources that are too broad (a bare https: allows every site on the internet), deprecated syntax and the common mistake of listing a host that is then overridden by 'none'. It also shows how the policy would treat a given script URL, which is the quickest way to check that the analytics will still load.
While you are setting headers, the HTTP headers checker reads what a live URL actually sends back, so you can confirm the header is present after deployment and is not being stripped by a proxy or CDN in between.
The directives that matter most
script-src: the one that stops XSS. No'unsafe-inline', no'unsafe-eval', no wildcards.object-src 'none': nobody needs Flash or plugins.base-uri 'self': stops an injected<base>tag redirecting every relative URL.frame-ancestors 'none'(or a specific host): replacesX-Frame-Optionsand prevents clickjacking.form-action 'self': stops injected forms posting credentials elsewhere.
The short version
Build the policy with a tool, deploy it report-only, add what the reports show is genuinely needed, replace 'unsafe-inline' with nonces or hashes, then enforce. It takes a couple of weeks of elapsed time and about an hour of work.