Magento already ships CSP: start monitoring it today

Magento 2 has shipped with Content Security Policy enabled by default since version 2.3.5. On current releases, most pages use a report-only policy, while payment pages use an enforced policy by default. Magento does not, however, know where to send violation reports unless you configure an endpoint. That means a valuable source of browser-side security telemetry is usually left unused, so let’s look at how to start collecting it.

There's an empty box

The reporting endpoint is a separate setting from the policy, which has a default configuration, so you have an empty box to fill in. Here's Magento_Csp's own etc/config.xml:

<csp>
    <mode>
        <storefront>
            <report_only>1</report_only>
        </storefront>
        <admin>
            <report_only>1</report_only>
        </admin>
    </mode>

report_only is set. There is no report_uri element at all because there's no sensible default for one, so the policy runs but the reports go nowhere.

You can check your own store by looking at the page load in the Network tab of Dev Tools and looking for the CSP header, or using a simple grep:

curl -sI https://yourstore.example/ | grep -i content-security-policy

If you get back a Content-Security-Policy-Report-Only header with no report-uri in it, that's the one you're looking for. Your visitors’ browsers have been evaluating this policy the whole time, but any violations have gone no further than the console in dev tools because there was nowhere to send them.

This isn't a handful of stores

We crawl the top million sites every day and record what security headers they're all sending out to their visitors. In the August 2026 crawl, 2,568 sites were taking card payments behind a report-only CSP with no reporting endpoint anywhere in the policy.

2,236 of those 2,568 sites are Magento, so that's 87% of every payment-taking site we found with this exact misconfiguration, all on this one platform. Crawling only the top one million sites also limits what we can see. Store Leads counts over 100,000 live Magento stores, but our crawl only sees the ones big enough to rank inside the top one million.

At first, the fix looks simple

Magento exposes four Report URI fields in its configuration, so at first this appears to be a straightforward configuration change. Go to Stores > Configuration > Security > Content Security Policy > Mode. There are four groups, each with a single Report URI field:

  • Storefront Default
  • Storefront > One Page Checkout
  • Admin Default
  • Admin > Create Order

On Magento 2.4.7 and later, using the default CSP configuration, two of these pages enforce their policies while the other two are report-only. Pointing all four at one address therefore merges violations that were blocked with violations that were only observed.

Support for the Reporting API

While building a module to make it easy to configure these settings, I went looking at how Magento renders the header, and found something that changes the advice we give.

Here is SimplePolicyHeaderRenderer::render(), which builds the header on every request:

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);
}

On a stock installation, the moment you set a report URI, Magento doesn't just add report-uri to your policy. It also adds a report-to directive, generates a Report-To header, and builds that header out of the same address you gave it. This assumes that you want one address to serve both mechanisms, but you might not, and Report URI has different addresses for each.

The classic report-uri mechanism POSTs a single application/csp-report body, and the Reporting API via report-to POSTs a batched application/reports+json array. The format and delivery mechanism are different enough that many collectors expose it as a separate endpoint, just like ours.

Per CSP Level 3, a browser that supports report-to uses that and ignores report-uri entirely, and that leaves us with a problem: Magento places the same address into both reporting mechanisms, but Report URI requires a different endpoint for each.

How to check your own store

Again, you can check the request in the Network tab of Dev Tools, or you can use a simple grep to fetch the headers.

curl -sI https://yourstore.example/ | grep -i '^report-to'

If you get a Report-To header back and the URL inside it is your CSP endpoint rather than your collector's Reporting API endpoint, you're affected. If you're a Report URI customer, the two look like this:

CSP:           https://yoursubdomain.report-uri.com/r/d/csp/reportOnly
Reporting API: https://yoursubdomain.report-uri.com/a/d/g

We have a different path for each configuration, and you need to set the right value for both.

What we've done about it

We’re addressing the issue in two places.

First, I've reported it upstream, because this isn't a problem with any one collector, and solving the issue at the source seems like the best approach. It affects every Magento store using any collector that separates the two endpoints like we do:

  • magento/magento2#41232 covers the Report-To header being built from report_uri.
  • magento/magento2#41233 is a second, smaller one found along the way: the four Report URI fields have no server-side validation, and a control character in one returns HTTP 500 for the whole storefront.

Second, we've published a small module that's MIT licensed and fixes both halves of this:

composer require report-uri/magento2-csp-reporting
bin/magento module:enable ReportUri_CspReporting
bin/magento setup:upgrade
bin/magento cache:flush

It's a single copy/paste from your Setup page and it fills in all four fields, giving each the disposition its page actually uses. It also repoints Report-To at the Reporting API endpoint and adds the modern Reporting-Endpoints header, so Chrome's reports arrive where they should. If your report-uri belongs to a different collector, it leaves Magento’s existing reporting endpoints unchanged.

The module also adds 'report-sample' and 'report-sha256' to script-src by default. The first includes a short sample of blocked code in violation reports, and the second enables CSP Integrity reporting in supporting browsers, providing hashes for scripts that execute rather than only reporting policy violations. Because that integrity telemetry is generated on page loads and consumes reporting quota, both settings remain configurable.

It's on GitHub and it's in beta, so try it out on staging first, because it changes response headers on every page. If you want to get a free Report URI account during the beta, and then 50% off your first year after the beta, check out the Magento Beta Program.

Why this really matters

If you take card payments, your checkout is a payment page in the sense PCI DSS 4.0.1 means it. Requirement 6.4.3 wants every script on that page inventoried and authorised, while requirement 11.6.1 wants changes to those scripts, and to the page's CSP, detected and acted on.

Magento already gives you the control, it enforces a policy on your checkout right now without you doing anything. What it doesn't do is keep any record of what that policy saw, what that policy blocked, or if that policy changed, and a control that keeps no evidence is difficult to put in front of a QSA.

Magento is also the platform Magecart was named after. A script appearing on a page it has never run on before, or data heading somewhere that was never authorised, is the earliest reliable signal that a skimmer has landed on your checkout. That signal is being generated on your store right now, by your customers' browsers, but it's being discarded before anyone sees it.


If you want to read more about how Magento handles CSP, our Adobe Commerce and Magento guide covers the configuration in full.

Read more