CSP Report Analyzer Guide: Read a CSP Violation Report

A Content-Security-Policy that nobody monitors is a policy whose failures are invisible. When a browser blocks content under CSP, it can send a violation report to an address the policy names, describing what was blocked, which directive blocked it, and which page was involved. Those reports are the feedback loop that makes CSP maintainable: without them, a policy that breaks a legitimate feature is discovered by visitors, while a policy that blocks an attack attempt leaves no trace at all. Learning to read a report is therefore not an optional extra. It is the skill that turns CSP from a one-time configuration into a policy that can be kept correct over time.

The CSP Report Analyzer tool pastes a CSP violation report in JSON and reads its blocked URI and violated directive. It works on pasted text, which means you copy a report, paste it in, and read a structured summary rather than picking fields out of raw JSON by eye. That is a genuine convenience when reports arrive in bulk, because raw reports are verbose and repetitive, and the two fields that matter most for triage, the blocked address and the violated directive, can be buried among fields that matter less. The analyzer does not fetch anything and does not judge whether the blocked source is legitimate; that judgement is yours, and this guide is about making it well.

A violation report has a predictable shape. The document-uri field names the page where the violation happened. The blocked-uri field names the address that was blocked, or the keyword inline or eval when the blocked content was inline code rather than a fetched address. The violated-directive field names the directive that did the blocking, such as script-src or img-src. The original-policy field records the policy that was in force, so a report can be matched to the policy version that produced it. The disposition field says whether the policy was in enforce mode, meaning the content was actually blocked, or report-only mode, meaning the content was allowed but the violation was reported anyway. Report-only mode is the key to safe rollouts: it generates the same reports without breaking the page, so a new policy can be tested against real traffic before it starts blocking anything.

Reading a single report is straightforward; reading a hundred requires grouping. The productive question is rarely what one report says, but what the reports say in aggregate. Group them first by violated directive, because that tells you which content type is affected, then by blocked address, because that tells you which source is involved. A group of many reports for one address under one directive is usually one forgotten legitimate source, and the fix is a single policy entry. A scatter of reports for many different addresses under script-src, especially addresses that look random or suspicious, is a different pattern, and deserves investigation as a possible injection attempt rather than a policy gap. Counting before acting is what separates a careful policy change from a panicked one.

A worked example you can follow

Take a sample set of 30 violation reports collected during a report-only rollout. Group them by violated directive: 18 reports name script-src, 9 name img-src, and 3 name style-src. That is 18 out of 30, or 60 percent, for scripts, 9 out of 30, or 30 percent, for images, and 3 out of 30, or 10 percent, for styles. The script group dominates, so it gets attention first. Now group the 18 script reports by blocked address: 15 name widgets.example-cdn.com, and 3 name inline. The 15 reports for one address are 15 divided by 18, or about 83 percent of the script group, pointing at a single forgotten source, the widget CDN, which the policy should allow by adding that named host to script-src. The 3 inline reports are 3 divided by 18, or about 17 percent, and they need a different response: either the inline scripts are legitimate, in which case they need nonces or hashes, or they are not, in which case they should stay blocked.

Now consider the image group: 9 reports, of which 7 name images.example-cdn.com and 2 name data: addresses. The 7 CDN reports, or about 78 percent of the group, point at another forgotten host, this time for img-src. The 2 data: reports, or about 22 percent, point at embedded images, which need a data: exception in img-src if they are legitimate. After adding the widget host, the image CDN host, and the data: exception, the expected report count for legitimate sources falls from 30 to the remaining unexplained reports. If a second collection then shows 3 reports instead of 30, the reduction is 30 minus 3, or 27 reports resolved, which is 27 divided by 30, or 90 percent of the original volume explained by 3 policy entries. That is a normal, healthy rollout pattern: a small number of entries resolves a large share of reports.

Take a contrasting sample: 12 reports under script-src naming 12 different addresses, each appearing exactly once, with addresses that look randomly generated. Here 12 divided by 12, or 100 percent, of the group is unique addresses, which is the opposite of the forgotten-host pattern. No single policy entry would explain them, and adding all 12 addresses to the policy would be a serious mistake, because it would allow a set of sources that nobody can vouch for. The right response is investigation: check whether the pages involved show signs of injection, whether the addresses appear in any legitimate code, and whether the pattern correlates with a particular page or campaign. Reports that resist grouping are often the most important ones, precisely because they do not fit the innocent explanation.

Mistakes to avoid

The first mistake is adding every blocked address to the policy without asking whether it is legitimate, which turns an attacker's address into an allowed source. The second mistake is reacting to a single report instead of grouping, which produces a policy full of one-off entries for sources that were never a pattern. The third mistake is sharing raw reports carelessly, because reports contain page addresses and sometimes fragments of content that may be sensitive; redact before sharing outside the team. The fourth mistake is deploying a policy in enforce mode without a report-only phase, which converts every forgotten source into a broken page discovered by visitors. Report-only first, group the reports, fix the legitimate gaps, and only then enforce.

The CSP Checker tool reads the policy that produced the reports, and the CSP Generator tool rebuilds it with the newly allowed sources included. For background on placing CSP in a wider security review, see Reading a Security Headers Report, Line by Line.

Checking the result before you change the policy

Treat an analyzed report as evidence, not as an instruction. For each group, confirm the source is legitimate by finding it in the site's own code or in a named provider's documentation, then add exactly that host to exactly the directive that blocked it, and re-collect reports to confirm the group shrinks. Keep a log of each policy change with the report counts before and after, because that log is what proves the policy is improving rather than merely changing. A report pipeline that is read, grouped, investigated, and acted on in that order turns CSP from a static header into a living policy that gets stricter where it can and stays accurate where it must.