gg
Home › Blog › A wrong Content-Security-Policy fails silently

A wrong Content-Security-Policy fails silently

2026-08-27 · 4 min read
securitywebhosting

A Content-Security-Policy is a set of rules the browser applies to one page. It states which origins that page may load scripts from, which it may load styles, images and fonts from, which destinations it may open network connections to, and which sites — if any — may put the page inside a frame. The server sends it as a response header, the browser enforces it, and anything the policy does not name is refused.

Refused, not reported. That distinction is the whole subject of this page.

The directives, in plain terms

default-src is the fallback: any kind of fetch you do not name separately falls back to it. Set it to 'self' and the page starts from same-origin-only, after which you open specific doors.

A policy is those directives joined with semicolons on one header line. As an example of the shape:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'

Nothing about that line is universally right, and it is not a setting to copy. It describes a site that loads everything from its own origin and inlines a little CSS. A site with an analytics script, a font from another host or an image CDN has to name those origins, and a policy borrowed from somewhere else will either refuse things it should not or permit things it need not.

Why a mistake in it is invisible

When a policy refuses a resource, the browser does not put an error in the page. There is no banner, no message, no styling to say a rule fired. An image loaded from a scheme the policy does not allow simply does not appear — a blank space, on a page that otherwise renders normally. A refused stylesheet leaves the page unstyled, which reads as a CSS bug. A refused script leaves a button that does nothing, which reads as a JavaScript bug. The page never explains itself.

Now add the part that turns this into a deployment surprise. Most local development servers send no CSP header at all. With no header there is no policy, so nothing is refused, so the page works perfectly on your machine — and it will keep working perfectly for as long as you only test it there. Your policy does not exist until the host that serves the real site attaches the header. The first time anyone sees the effect of the policy is usually the first time a visitor does.

Design around that rather than discovering it. Serve the same header locally if you can, or at least load the deployed preview once with the console open.

Schemes are sources too, which is where data: and blob: part company

img-src 'self' data: lists two sources. 'self' means the page's own origin. data: is not an origin at all — it is a scheme, and naming it permits any URL beginning with it. So an image inlined straight into the markup as <img src="data:image/png;base64,iVBORw0KGgo"> loads fine.

A blob: URL is a different scheme, and data: does not cover it. Blob URLs are what URL.createObjectURL() hands back: the ordinary way to display a file the user just picked, a canvas export, or an image assembled in JavaScript. Under img-src 'self' data: every one of those is refused, and refused the silent way — an empty box where a picture should be, in the one part of the site that a glance at the homepage never exercises. If the site does that, the directive needs blob: named as well.

So check the console

The page will not tell you. The browser will. Every refusal logs a violation in the developer console naming the directive that fired and the URL that was blocked. Open the console against the deployed site and walk the paths that load something conditionally: the file picker, the embedded map, the print view, the page that only renders after signing in.

There is also a rehearsal mode. Send the same policy under the header name Content-Security-Policy-Report-Only and the browser enforces nothing, but logs every violation it would have caused. Run a candidate policy that way against real traffic, wait until the log goes quiet, then change the header name.

The tool

The CSP Builder composes a policy from plain choices — extra script and style origins, whether inline scripts and inline styles are allowed, whether images are same-origin only, same-origin plus data:, or any HTTPS origin, and whether other sites may frame you. It prints the result twice: once as the header line, and once as the <meta http-equiv> equivalent for when you cannot set headers. It runs on your device, and it ends with exactly the instruction this page has been arguing for — open the browser console, because a violation logs the directive to fix and the page never will.

Try it free — with the steps shown

The CSP Builder runs in your browser and shows exactly how it got the answer, so the method sticks.

Open CSP Builder

More from the blog