Vendor code
Third-party scripts you did not approve
Almost nobody knows everything running on their own site. A tag manager loads three tags, one of those loads a partner's loader, and that pulls a fourth from somewhere nobody has heard of — all after your build finished, all without a deploy.
A reporting policy answers the question from the browser, which is the only place the full chain is visible. This page is about what those reports contain, and the large category of vendor change they will never show you.
What the problem is
Third-party script risk is not usually malice. It is that the thing you approved is not the thing that is running. You reviewed a vendor's tag in 2023; since then it has been rewritten twice, acquired once, and it now loads a CDN bundle that loads a consent tool that loads a session recorder. Every link in that chain executes with the same privileges as your own code, and none of it appears in your repository.
The chain is also the part that is genuinely hard to audit by hand. Opening devtools tells you what loaded on your machine, on that page, at that moment, with your ad blocker on. Scripts that load conditionally — a geography, a logged-in session, a checkout step — do not show up, and the ones that fire for one visitor in a thousand never will.
A report-only script-src listing the hosts you believe you use turns that into an inbox. Every host that is not on the list reports itself, from real visitors, on real pages, in whatever conditions those visitors happen to be in.
What the report looks like
This is the second hop: your policy allows the vendor's widget, the widget fetches something else, and the something else is not on the list.
{
"csp-report": {
"document-uri": "https://shop.example.com/checkout/payment",
"referrer": "",
"violated-directive": "script-src-elem",
"effective-directive": "script-src-elem",
"original-policy": "default-src 'self'; script-src 'self' https://widget.vendor.example; report-uri https://ingest.headerhawk.com/r/abc123",
"disposition": "report",
"blocked-uri": "https://cdn.partner-analytics.example/loader.js",
"source-file": "https://widget.vendor.example/embed.js",
"line-number": 1,
"column-number": 18422,
"status-code": 200,
"script-sample": ""
}
}How it appears in HeaderHawk
script-src-elem blocking cdn.partner-analytics.example
- source-file is the whole point of this report. It names the script that made the request — the vendor's bundle, not your page — which is what turns 'something is loading an unknown host' into 'this tag is loading an unknown host'.
- line 1, column 18422 is normal, not a bug: the vendor ships minified, so every request in the file is on line one. The column is still enough to find the call in a beautified copy.
- blocked-uri is the new host. Note that the vendor's own script produced no report at all — it is on the allowlist, so nothing was violated. You only ever see the hop past the edge of what you approved.
- One issue per directive and host, so a vendor that adds four new endpoints is four rows with counts, not four thousand reports. First-seen is roughly when the vendor shipped the change.
- The page count on the issue tells you scope. A host on every page is a globally loaded tag; one confined to the checkout is a feature, or a problem.
Field by field: the script-src reference.
What HeaderHawk does with it
The first week of a report-only policy on a site with any marketing history is a long list, and the value is in it being a list rather than a stream. Reports collapse to one row per directive and blocked host, each with a report count, the number of distinct pages it appeared on, and first- and last-seen timestamps. Working through it is a triage exercise: recognise it and add it to the policy, or do not recognise it and go and ask someone.
Reports from browser extensions are filtered out of that list's priority for you — anything whose blocked URI is achrome-extension:// or equivalent scheme, or whose user agent matches a known crawler, is classified as noise. It stays visible, because occasionally that is what you are looking for, but it never triggers an alert. On a public site this is a large fraction of the traffic.
Once the list is quiet, the remaining reports are the interesting ones: a new host means something changed on a page you did not deploy. Alert rules watch for that — a 15-minute window reported by two or more browser families, or with twenty or more reports from one, notifies Slack, email or a webhook immediately, and quieter ones roll into a daily or weekly digest. A deduplication window, 24 hours by default, keeps an ongoing issue to one notification.
On Team and above, the Scripts view lists the SHA-256 hashes of the scripts browsers actually executed, grouped by origin and by script URL. That is a different question — not "what would my policy refuse" but "what ran" — and it is where a second hash against a familiar URL shows a vendor has shipped an update. It is a sample, it is Chromium-only, and it raises no alerts; the limits below are not footnotes.
Where CSP alone is not enough
The gap on this page is specific and worth stating twice: reporting tells you when a new host appears. It is much weaker on an old host serving new code.
- A change inside a script you already allow reports nothing. This is the limit that matters most here. Your allowlist names hosts, and a vendor pushing new code to the same host has not violated it. The Ticketmaster breach is exactly that shape.
- CSP describes origin, not behaviour. It cannot tell you that an allowed analytics script started reading your form fields, as long as it sends them somewhere you already permit. Narrowing the policy on sensitive pages is the lever; the policy itself has no opinion about what code does.
- There is no alert when a new script appears or an existing one changes. Executed-script hashes are collected for inspection in the dashboard, and only the violation path raises notifications. You will see a second hash against a familiar URL when you look; nothing pages you.
- The hash inventory is a sample and starts on the Team plan. Chromium 133 and above only, fetched subresources only — never cached or inline scripts — and repeat sightings are sampled. A first sighting skips the sample rate as a priority rather than a guarantee: a site over its monthly quota or its daily hash ceiling can have a new hash held over to the next sighting. Free tenants have no Scripts view at all.
- Report-only mode prevents nothing. It is a way to find out what an enforcing policy would refuse, before you enforce it. The block, when you switch, is your own header.
- A tag manager that needs 'unsafe-inline', or an allowlist wide enough to keep marketing unblocked, gives up most of the signal by design. That is a real trade-off and worth making deliberately rather than discovering later.
- It does not monitor your response headers, so it cannot tell you that a policy stopped being served. Reports going quiet looks the same as a quiet week.
Where to go next
- The
script-srcreference covers the allowlist itself, including why your reports namescript-src-elemwhen your policy does not. - Magecart and card skimming is what this becomes when the page in question takes payment.
- PCI DSS 4.0 covers the script inventory and change-detection requirements, and which parts of them nothing here addresses.
Find out what is actually on your pages
List the hosts you think you use, put them in a report-onlyscript-src, and read what comes back.