How to read a CSP violation report
CentralCSP Team ·
Last update:
You turned on Content Security Policy (CSP) reporting, the reports are arriving, and now you are staring at JSON trying to work out what blocked what. The field names are terse, some values are redacted, and the same event can arrive in two different shapes depending on how you wired reporting. This is a field-by-field guide to a CSP violation report so you can read one at a glance and know which line of your policy to change.
There are two formats for the same underlying event. The modern
csp-violation report is delivered
through the Reporting API as part of a batch; the legacy report-uri format is a
single object the browser POSTs on its own. They carry the same information under
different field names, so once you can read one you can read the other.
The modern shape, csp-violation
This is what you get when the policy uses the report-to directive. The browser
delivers an array with Content-Type: application/reports+json, and the
CSP-specific data sits in body with camelCase field names.
{
"type": "csp-violation",
"age": 53,
"url": "https://api-next.centralcsp.com/",
"user_agent": "Mozilla/5.0 ...",
"body": {
"documentURL": "https://api-next.centralcsp.com/",
"referrer": "https://www.google.com/",
"blockedURL": "https://apis.google.com/js/platform.js",
"effectiveDirective": "script-src-elem",
"originalPolicy": "default-src 'self'; report-to csp-endpoint",
"sourceFile": "https://api-next.centralcsp.com/",
"sample": "",
"disposition": "enforce",
"statusCode": 200,
"lineNumber": 1441,
"columnNumber": 59
}
}| Field | What it means |
|---|---|
documentURL | The page where the violation happened. |
blockedURL | The resource that was blocked. Cross-origin, this is truncated to scheme, host, and port. |
effectiveDirective | The directive that actually fired (for example script-src-elem). |
originalPolicy | The full policy text the browser enforced. |
disposition | enforce for a blocking policy, report for Report-Only. |
sample | First ~40 characters of the offending inline content, only when the directive carries 'report-sample'. |
statusCode | HTTP status of the document response. |
lineNumber / columnNumber | Location for inline violations. |
Two of these surprise people. blockedURL is deliberately redacted to the origin
for cross-origin resources, so you see https://cdn.example/ rather than the exact
path; that is a privacy measure, not a bug. And effectiveDirective is the specific
directive that fired, which can be more granular than what you wrote: a blocked
<script> tag reports script-src-elem even if your policy only set script-src,
because script-src is the fallback the granular directive inherits from.
script-src-elem vs script-src-attr
walks through that split and what each granular directive controls.
If you parse these at your endpoint, here is the modern body as a TypeScript type you
can drop into the handler. Hover any field to see its type; disposition is a union
of the only two values it ever holds.
// The body of a modern csp-violation report.
interface CspViolationBody {
: string;
: string;
: string;
: string;
: string;
: string;
: string;
: "enforce" | "report";
: number;
: number;
: number;
}
interface CspViolationReport {
: "csp-violation";
: number;
: string;
: string;
: CspViolationBody;
}
function (: CspViolationReport) {
const = ..;
const = .. === "enforce";
return { , };
}The legacy shape, report-uri
When the policy uses the report-uri directive, the browser posts a single object
with Content-Type: application/csp-report and kebab-case field names.
{
"csp-report": {
"document-uri": "https://api-next.centralcsp.com/",
"blocked-uri": "https://apis.google.com/js/platform.js",
"effective-directive": "script-src-elem",
"violated-directive": "script-src-elem",
"original-policy": "default-src 'self'; report-uri /csp-reports",
"disposition": "enforce",
"status-code": 200,
"script-sample": ""
}
}The mapping is one to one: blockedURL is blocked-uri, effectiveDirective is
effective-directive, sample is script-sample, and so on. The one extra field,
violated-directive, is a historic alias for effective-directive and can be
ignored. If your endpoint accepts both formats, branch on the Content-Type header.
Which directive you should be sending, and why you often send both, is covered in
report-uri vs report-to.
Reading a report in practice
Read effectiveDirective and blockedURL together; that pair tells you almost
everything. A script-src violation pointing at a host you recognize (a CDN, an
analytics provider) usually means your policy is too strict and you need to allow
that source. A violation pointing at a host you do not recognize, or an inline
sample you did not write, is worth investigating as a possible injection. To trace a single violation back to the line that caused it, debug CSP violations in Chrome DevTools.
The sample field is the most useful for inline violations, but it only appears
when the directive carries the 'report-sample' keyword, and it is capped at about
40 characters, enough to identify the snippet, not to leak a secret. disposition
tells you whether this was a real block (enforce) or a dry run (report), which
matters when you are running an enforced policy and a Report-Only policy at once.
Treat every field as attacker-influenced input. A report is data the page caused the
browser to send, so never render blockedURL or sample into an admin page without
escaping.
Stop reading raw JSON
Reading one report is fine; reading thousands is not. CentralCSP ingests both shapes, normalizes them into one model, and groups violations by directive and blocked host, so instead of a log file you get a ranked list of what is actually breaking and how often. It shows them on a dashboard you can act on, which is the difference between collecting reports and using them.
Next steps
- Set reporting up in Get started with CSP reporting.
- See the envelope in the report delivery format.
- Tighten the policy that produced these with the CSP evaluator.
Collect and group CSP reports automatically.