CSP Report-To header is built from report_uri, silently losing reports from Reporting API browsers
Preconditions and environment
Magento Open Source 2.4.9. Present since 2.4.7, and in the backports: 2.4.4-p9, 2.4.5-p8, 2.4.6-p6.
A CSP report URI configured under Stores > Configuration > Security > Content Security Policy, pointing at any collector that serves the Reporting API on a different address to its classic report-uri endpoint.
Steps to reproduce
Set Storefront Default > Report URI to a collector's report-uri endpoint.
bin/magento cache:flush
Request any storefront page and inspect the response headers:
curl -sI https://store.example/ | grep -iE '^(report-to|content-security-policy)'
Expected result
The report-to group resolves to an endpoint capable of accepting the Reporting API's application/reports+json payload — either by not emitting report-to at all, or by allowing the Reporting API endpoint to be configured separately from report_uri.
Actual result
Magento\Csp\Model\Policy\Renderer\SimplePolicyHeaderRenderer::render() builds the Report-To header out of the report_uri value:
if ($config->getReportUri() && !$response->getHeader('Report-To')) {
$reportToData = ['group' => 'report-endpoint', 'max_age' => 10886400,
'endpoints' => [['url' => $config->getReportUri()]]];
$value .= ' report-uri ' . $config->getReportUri() . ';';
$value .= ' report-to ' . $reportToData['group'] . ';';
$response->setHeader('Report-To', json_encode($reportToData), true);
}This assumes one address serves both mechanisms. It usually does not. The classic mechanism posts application/csp-report to the report-uri endpoint; the Reporting API posts a batched application/reports+json array to a Reporting API endpoint, and collectors commonly expose these as two separate addresses.
Per CSP Level 3, a user agent that supports report-to uses it and ignores report-uri entirely. So on a correctly configured store, Chromium browsers post to an endpoint that cannot accept the payload, while Firefox and Safari continue to work via report-uri.
Nothing surfaces the failure. The configuration looks correct, no error is raised, and the merchant sees a partially populated report set with no indication that a browser family is missing from it.
Additional information
Two things make this hard to notice: the store looks correctly configured, and the browsers that still work are the ones producing the smaller share of traffic. It shows up as odd data rather than as a broken configuration.
Suggested fixes, in order of preference:
- Add a separate report_to configuration field per area, falling back to report_uri only when it is unset.
- Emit the report-to directive and Report-To header only when such a field is set.
- At minimum, document that the two must resolve to the same address.
A plugin on the renderer works around it, but every affected collector's users need the same workaround independently, which suggests it belongs in core.
Release note
No response
Triage and priority
- Severity: S0 - Affects critical data or functionality and leaves users without workaround.
- Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
- Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
- Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
- Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “usability”.
Source: magento/magento2