CSP Checker Guide: Read a Content-Security-Policy Header

Cross-site scripting is one of the oldest and most damaging web vulnerabilities, and its core trick is simple: get a victim's browser to run attacker-controlled code in the context of a trusted site. The browser, by default, runs scripts from wherever the page tells it to, which means a single injected snippet can steal data or act as the user. Content-Security-Policy, usually shortened to CSP, changes that default. It is a header that tells the browser, in advance, which sources are allowed for each kind of content: scripts from here, styles from there, images from anywhere on this list, and nothing from anywhere else. Anything not on the list is blocked, and the block is enforced by the browser itself, not by the page's own code, which is what makes it a genuine second line of defence.

The CSP Checker tool pastes a Content-Security-Policy header and checks its directives for risky values. It works on pasted text, not on a live fetch, so it is a drafting and review tool: you paste the header you intend to serve, and the tool reports missing directives and values that undermine the policy. That boundary is worth keeping in mind throughout, because a header that reads well in a paste still has to be served on the real response, and a header served on one response does not automatically cover the rest of the site. The paste check catches drafting errors; the deployment check confirms protection.

A CSP header is a list of directives separated by semicolons, and each directive names a content type and its allowed sources. The most important directive is default-src, which sets the fallback for any content type that has no directive of its own. If default-src is set to self, meaning only the site's own origin, then scripts, styles, images, and everything else default to self unless a more specific directive says otherwise. The specific directives include script-src for scripts, style-src for styles, img-src for images, connect-src for network requests from scripts, font-src for fonts, frame-src for embedded frames, and object-src for plugins and embedded objects. A policy that sets object-src to none is a widely recommended hardening step, because plugins are a historically exploited content type, and none is the strictest possible value.

The risky values are where a CSP check earns its keep. The value unsafe-inline allows inline scripts and styles, which is exactly the kind of content an injection attack produces, so a script-src that includes unsafe-inline largely defeats the purpose of having a script policy at all. The value unsafe-eval allows code constructed from strings at runtime, which is another classic injection vector. The wildcard star allows any origin, which turns an allowlist into no list at all for that directive. And the value data: allows content embedded as data URLs, which can be abused to smuggle scripts past an origin-based policy. None of these values is always wrong; there are pages that genuinely need inline styles during a migration, for example. But each one should be a conscious, temporary, documented choice, not a value copied from an example and forgotten.

A worked example you can follow

Take a sample header with 6 directives: default-src self; script-src self; style-src self; img-src self data:; object-src none; frame-src none. Count the directives: 6. Count the risky values: img-src includes data:, which is 1 risky value out of 6 directives, or about 17 percent of the policy carrying a known risk. The rest of the policy is strict: script-src self blocks third-party scripts entirely, object-src none blocks plugins entirely, and frame-src none blocks all frames. The data: value in img-src is a common, often acceptable trade-off, because many sites embed small images as data URLs, but it should still be noted as the one deliberate exception, not left as an unexamined default.

Now take a second sample with 5 directives: default-src self; script-src self unsafe-inline; style-src self unsafe-inline; img-src star; object-src self. Count the risky values: unsafe-inline appears in 2 directives, and the wildcard star appears in 1, giving 3 risky values out of 5 directives, or 60 percent of the policy carrying risk. The script policy is weakened by unsafe-inline, the style policy is weakened the same way, and the image policy allows any origin, so 3 of the 5 content types are effectively unprotected. The fix is to remove unsafe-inline from both directives, which requires moving inline code into external files or protecting it with nonces or hashes, and to replace the image wildcard with an explicit list, such as self plus the named image host. After the fix, the risky count falls from 3 to 0, and a re-check of the full header confirms 0 risky values out of 5 directives.

Take a third sample with only 2 directives: default-src self; object-src none. A short policy is not necessarily a weak one, because every content type without its own directive falls back to default-src self. So scripts, styles, images, and fonts all default to self, which is strict, and object-src none adds a stricter rule for plugins. Counting directives alone would mislead: 2 directives here protect more than the 5-directive sample above, because the fallback does the work. The lesson is to read a CSP by its effective policy, directive by directive including fallbacks, rather than by its length.

Mistakes to avoid

The first mistake is including unsafe-inline in script-src without realising it largely defeats the policy, often because it was copied from a getting-started example. The second mistake is using a wildcard star for convenience during development and shipping it to production, where it silently removes protection for that content type. The third mistake is checking the header for one page and assuming the whole site is covered, when each response needs its own policy. The fourth mistake is treating a pasted-header pass as proof about the live site. Draft checks catch risky values and missing directives; only a check of the served response confirms what visitors' browsers actually enforce, and only real browsing, watching for blocked content, confirms the policy does not break the page.

The CSP Generator tool builds a header from allowed sources per directive, which is the natural next step after learning to read one, and the CSP Report Analyzer tool helps you read the violation reports a deployed policy produces. For background on placing CSP in a wider security review, see How to Configure HTTP Security Headers.

Checking the result before you publish it

Treat a clean paste check as permission to test, not to ship blindly. Serve the policy in report-only mode first if you can, collect the violation reports it generates, and count what would break: how many blocked resources, on which pages, from which sources. Fix the legitimate sources by adding them explicitly, rather than by loosening the policy with a wildcard, and only then switch to enforcement. Keep the header text and a note of every deliberate exception, such as data: in img-src, together in your configuration notes, because the next person to edit the policy will otherwise have to rediscover why each exception exists. A CSP that is read directive by directive, tested against real content, and confirmed on the live response is a genuine second line of defence.