All posts

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
  }
}
FieldWhat it means
documentURLThe page where the violation happened.
blockedURLThe resource that was blocked. Cross-origin, this is truncated to scheme, host, and port.
effectiveDirectiveThe directive that actually fired (for example script-src-elem).
originalPolicyThe full policy text the browser enforced.
dispositionenforce for a blocking policy, report for Report-Only.
sampleFirst ~40 characters of the offending inline content, only when the directive carries 'report-sample'.
statusCodeHTTP status of the document response.
lineNumber / columnNumberLocation 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

Collect and group CSP reports automatically.

Sources