How CSP script hash reporting works with report-sha256
Add 'report-sha256' to a Content-Security-Policy and Chrome and Edge send a hash of each script they fetch. How to set it up, what the report contains, and where it helps.
Michael Cameron4 min readUpdated
Most Content Security Policy reports tell you what the browser blocked. Script
hash reporting tells you what it loaded. Add 'report-sha256' to your policy
and Chrome and Edge send a report for each script they fetch, with a SHA-256
hash of the script’s contents.
Your policy becomes a record of the scripts that ran on your pages, the starting point for a script inventory and for the PCI DSS 4.0 script requirements.
What report-sha256 does
'report-sha256' is one of three keywords CSP Level 3 adds for script hash
reporting, along with 'report-sha384' and 'report-sha512'. Each one sets the
algorithm the browser uses for the hash, and each goes in a directive that
controls scripts, such as script-src or script-src-elem.
The keywords don’t allow or block anything. They look like hash sources such as
'sha256-…', which permit a script whose contents match, but a report keyword
only asks the browser to tell you what it loaded. The browser computes one hash
per report, so list one keyword.
How to set up script hash reporting
Script hash reports are delivered through the Reporting API, so they need
report-to and a Reporting-Endpoints header naming an HTTPS endpoint. The
older report-uri directive can’t deliver them.
Reporting-Endpoints: csp-endpoint="https://your-endpoint.example/"
Content-Security-Policy-Report-Only: script-src 'self' https://cdn.example.com 'report-sha256'; report-to csp-endpoint
The browser sends hash reports whether the policy is enforced or report-only, so
you can start with Content-Security-Policy-Report-Only and see which scripts
run before you tighten anything.
Cross-origin scripts need CORS
For a script served from another origin, the browser can only hash it if it was
fetched with CORS. Add crossorigin="anonymous" to the script tag, and check
that the server hosting the script sends an Access-Control-Allow-Origin
header. A CORS request to a server that doesn’t send one fails, and the script
won’t load. Without CORS, the browser still reports the script, but with an
empty hash.
<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>
What a csp-hash report looks like
Reports arrive as a JSON array in the standard Reporting API format, sent with
Content-Type: application/reports+json. Each entry has a type of csp-hash,
the page url, the user_agent, the report’s age, and a body that
identifies the script and carries its hash.
{
"type": "csp-hash",
"age": 0,
"url": "https://example.com/page",
"user_agent": "Mozilla/5.0 (…) Chrome/…",
"body": {
"documentURL": "https://example.com/page",
"subresourceURL": "https://cdn.example.com/script.js",
"hash": "sha256-<base64 digest of the script>",
"destination": "script",
"type": "subresource"
}
}
The body has a type of its own, subresource, alongside the envelope’s
csp-hash. The hash value uses the same <algorithm>-<base64> format as a
hash source, so you can compare it directly with the hashes in your policy or
your build output. Hash reports only go to your reporting endpoint. A
ReportingObserver on the page never sees them.
Browser support
Chrome and Edge support the report-sha keywords, from Chrome 133 in February 2025. Support for Safari is being developed in WebKit. Firefox doesn’t support them.
What script hash reports don’t include
Hash reports only cover scripts the browser fetches. A script served from the browser’s cache isn’t fetched, so it isn’t reported, and a site with long cache lifetimes sees fewer hash reports than it has scripts. Inline scripts written into the page aren’t fetched either. They show up in violation reports instead, when your policy blocks them.
Script hash reports and PCI DSS 4.0
PCI DSS 4.0 requirement 6.4.3 asks for an inventory of the scripts on payment pages, and 11.6.1 asks for a way to detect unauthorized changes to those pages. Hash reports give you evidence for both: a list of the scripts that ran, and a hash that changes whenever a script’s contents do. Match those hashes against the files you expect to ship to tell an update you made from one you didn’t.
The PCI DSS page covers how HeaderHawk maps to each requirement.
How HeaderHawk handles script hash reports
HeaderHawk records the hash and its algorithm, the script’s URL and host, and the page it ran on. On the Team plan and above, the Scripts view groups them by origin and by script, and on the Professional plan and above by page too.
New hashes skip sampling. The first time a hash is reported for your site, HeaderHawk records it, and repeat reports of a hash it already knows are sampled at the site’s rate, 10% by default. The result is an inventory of the scripts your Chrome and Edge visitors ran, and a script whose contents change shows up as a new hash against a URL you already know.
On the Professional plan and above, the scripts on the pages you mark as payment pages can each be authorized or rejected, with a written justification kept alongside who decided and when. A script new to the site that runs on one of those pages raises an alert, and so does a new hash for a script authorized with pinned hashes, once browsers on two different networks have reported it. The daily scan of those pages closes the gaps this post describes for the scripts their HTML names: it fetches each one, cached or not and with or without CORS, hashes it the same way the browser does, and alerts on what it finds without waiting for a second network. It runs no JavaScript, so a script another script adds is still only as visible as the reports make it.
Setup instructions for your site are in the script integrity docs.