2026-09-05
A Content Security Policy tells the browser which sources of script, style and other resources a page may use. Written all at once, it usually blocks something you forgot about. Rolled out in steps, it rarely does.
Start with a strict policy that the browser reports on but does not enforce:
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; report-to csp
Reporting-Endpoints: csp="/csp-report"
Leave it for a week. Every violation arrives as a JSON report naming the directive and the blocked URL. Most of them are legitimate resources to add to the policy; some are browser extensions injecting scripts, which you can ignore.
Inline <script> blocks are the usual obstacle. Allowing
'unsafe-inline' defeats most of the protection, so give each inline script a nonce
generated per response:
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'self' <script nonce="r4nd0m">...</script>
The nonce must be unpredictable and different on every response, which means the HTML cannot
be cached as a static file. For a fully static site, hashes ('sha256-...') of each
inline script do the same job without that cost.
'strict-dynamic' lets a trusted script load further scripts, so a loader with a
nonce can pull in its dependencies without listing every domain.
Switch the header name to Content-Security-Policy and keep the reporting endpoint.
New violations after a deploy usually point to a third-party snippet someone added without
telling anyone.
object-src 'none': plugins are gone, and this closes an old injection route.base-uri 'self': stops an injected <base> tag from redirecting relative URLs.frame-ancestors 'none': the modern replacement for X-Frame-Options, against clickjacking.