Search by

hryvinskyi / magento2-csp

hryvinskyi

Content Security Policy management for Magento 2: admin whitelist per store and area, violation reports with conversion, semantics-preserving header optimisation and splitting, script hashes for CMS content

Package info

github.com/hryvinskyi/magento2-csp

Type:magento2-module

pkg:composer/hryvinskyi/magento2-csp

Statistics

Installs: 10 094

Dependents: 0

Suggesters: 0

Stars: 13

Open Issues: 0

2.0.1 2026-10-15 23:32 UTC

README

Latest Stable Version Total Downloads License

Hryvinskyi_Csp manages the Content Security Policy of a Magento 2 store from the admin: a whitelist of allowed sources per store view and area, collection of the violations browsers report, one-click conversion of reports into whitelist entries, and post-processing of the policy header that keeps it small without changing what it allows.

Upgrading from 1.x? Read the 2.0.0 upgrade notes first.

Features

  • Whitelist of hosts, schemes and hashes per directive, store view and area (storefront, admin or both). Entries are validated: no keywords, no *, no CSP3 sub-directives, no stores that do not exist.
  • Violation reports from the browser's legacy report-uri format and the Reporting API, grouped by governing directive, blocked value, store view and area; size-limited and rate-limited per client.
  • Conversion of report groups into whitelist entries. Inline code, eval, * and data:/blob: in directives that load code are refused. An existing entry with the same directive, value and area gains the store instead of being duplicated.
  • Import of whitelist entries from CSV and XML. The grid's CSV export imports as it is: stores are written as store view codes or All Store Views, and the importer also reads store ids and names, area labels and Enabled/Disabled.
  • Header optimisation: duplicate sources, hosts covered by a wildcard, https:// on secure pages and directives that allow exactly what their fallback allows are removed. Every step is checked against an independent CSP3 evaluator in the test suite. Replacing subdomains with a wildcard is the one opt-in step that widens the policy, and only for domains you list as trusted.
  • Header splitting for policies above a size limit: the parts together allow exactly what the single header allows. When that is impossible the header is sent whole and the admin is told.
  • Script hashes: a console command renders CMS pages, CMS blocks and configuration values as the storefront does and allows their inline scripts by hash.
  • Report cleanup by age or by count, nightly and from the console.
  • Template API: allow hosts or hashes for the current page from a template.

Requirements

  • Magento Open Source or Adobe Commerce 2.4.6 – 2.4.9
  • PHP 8.1 – 8.5, as far as the Magento version allows (2.4.9 runs on 8.3 – 8.5), with the dom, intl, json, libxml and mbstring extensions

Installation

composer require hryvinskyi/magento2-csp
bin/magento module:enable Hryvinskyi_Csp
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy

Admin

System > Content Security Policy has three pages:

  • Whitelist – add, edit, import, export and delete entries. Each entry has a directive, a value (a host or scheme source, or a base64 digest with its algorithm), store views and an area. The grid flags duplicates, entries covered by a wildcard in the same scope, and hashes that do not match their script.
  • Violation Reports – report groups with their reports. Convert to Whitelist allows the value for the group's store view and area; Skip keeps recording without an alert; Deny stops recording. System Messages tells each admin who may view reports how many violations were recorded since they last opened this page; opening it hides the message until the next new violation (Notify about new violations turns it off).
  • Configuration – Stores > Configuration > Security > Content Security Policy.

Permissions are under System > Permissions > User Roles > Role Resources > System > Content Security Policy: viewing and editing the whitelist, deleting and importing entries, viewing reports, changing their status, and converting them (converting also needs the whitelist edit permission).

Violation reports

With Collect violation reports on, the report URI of every page points to this module: storefront pages report to /csp_report_watch of their own store view, admin pages to /csp_report_watch/admin/index of the admin host. This replaces the Report URI set in core's Mode groups while it is on. A report is recorded only when its page belongs to the store view (or admin) that receives it; extension-injected resources are ignored, and query strings and fragments are removed from stored URLs.

The rate limit counts violations per client IP address (IPv6 clients per /64 network). Behind a proxy, load balancer or CDN every request comes from the proxy unless Magento knows which header carries the visitor's address and which addresses are your proxies. Configure both, for example in app/etc/di.xml:

<type name="Magento\Framework\HTTP\PhpEnvironment\RemoteAddress">
    <arguments>
        <argument name="alternativeHeaders" xsi:type="array">
            <item name="x-forwarded-for" xsi:type="string">HTTP_X_FORWARDED_FOR</item>
        </argument>
        <argument name="trustedProxies" xsi:type="array">
            <item name="varnish" xsi:type="string">10.0.0.5</item>
            <item name="load-balancer" xsi:type="string">10.0.0.6</item>
        </argument>
    </arguments>
</type>

With alternativeHeaders alone Magento takes the first address in X-Forwarded-For, which the visitor writes: a client that sends a new address with every report gets a new allowance each time. With trustedProxies Magento takes the last address that is not one of your proxies. List every proxy address exactly (ranges are not supported). If every request passes through a proxy that sets a header itself, such as CF-Connecting-IP, use that header (HTTP_CF_CONNECTING_IP) instead.

Allowing sources from templates

Hryvinskyi\Csp\ViewModel\DynamicCspProvider adds sources to the policy of the page being rendered:

<block name="map" template="Vendor_Module::map.phtml">
    <arguments>
        <argument name="csp" xsi:type="object">Hryvinskyi\Csp\ViewModel\DynamicCspProvider</argument>
    </arguments>
</block>
$block->getData('csp')->addScriptSrc(['https://maps.example.com']);
$block->getData('csp')->addFrameSrc(['https://maps.example.com']);

Services can use Hryvinskyi\Csp\Api\DynamicPolicyRegistryInterface::allow($directive, $hosts, $hashes, $self). Hashes may be a list of sha256 digests or [digest => algorithm] (sha256, sha384, sha512). Invalid directives, hosts and hashes are logged and skipped, so a template never fails because of them. Only the directives Magento renders are accepted (script-src, style-src, img-src, frame-src, …), never a CSP3 sub-directive such as script-src-elem: adding one would stop it falling back to its parent.

Sources added this way apply to the page that rendered the template. Magento keeps them with the cached block HTML, and Varnish keeps the whole header with the page. With the built-in full-page cache, a cache hit renders the policy again without rendering blocks, so sources added from templates are missing on cached pages; whitelist such sources in the admin instead.

For inline scripts prefer Magento\Csp\Api\InlineUtilInterface / SecureHtmlRenderer (core) or, on Hyvä, HyvaCsp::registerInlineScript(). Hryvinskyi\Csp\ViewModel\CspNonceProvider::getNonce() returns a per-request nonce and must only be used on pages that are never cached.

Script hashes

bin/magento hryvinskyi:csp:generate-script-hashes [--store=1] [--type=page --type=block --type=config] [--yes]

The command renders each active CMS page and block of the store view (including those of all store views) with the CMS template filter of the store view, and reads the configuration values that apply to it (including values locked in app/etc/env.php or app/etc/config.php). Every inline script that a hash can allow (no src, no nonce, a JavaScript type, no form key or other per-request value) is shown with its hash and, after confirmation or with --yes, allowed for that store view on the storefront. Add -v to print each script.

Header optimisation and splitting

All steps are off by default and work on the final header Magento renders.

Setting Effect
Optimize policy headers Master switch; removes duplicate sources
Remove hosts covered by a wildcard www.example.com goes when *.example.com in the same directive covers it (same scheme, port and path)
Remove https:// from hosts on secure pages https://cdn.example.com/p becomes cdn.example.com/p on HTTPS pages only
Remove redundant directives A fetch directive goes when its fallback allows exactly the same for every kind of request
Replace subdomains of trusted domains with a wildcard Widens the policy: N subdomains of a listed trusted domain become *.domain
Split large policy headers Splits above the size limit into headers that together allow exactly the same

Splitting repeats report-uri in every header and keeps default-src in one of them, which also restricts prefetch and prerender requests. That header gets a permissive source list for each directive another header restricts, so the other header decides; the headers together are larger than the single header. If one group of related directives alone exceeds the limit, the header is sent unsplit and System Messages shows it: check the response header size against your web server and proxy limits (curl -sI https://your-store/ | wc -c).

Report cleanup

Enabled by default: every night reports not seen for 30 days are deleted, together with pending report groups left without reports. Change the mode (by date or by record count) and threshold under Violation Report Cleanup.

bin/magento hryvinskyi:csp:report:clean                        # configured mode and threshold
bin/magento hryvinskyi:csp:report:clean --mode=count -t 1000    # keep the 1000 most recently seen
bin/magento hryvinskyi:csp:report:clean --dry-run               # only count

Support

Open an issue on GitHub.

License

MIT

Author

Volodymyr Hryvinskyi volodymyr@hryvinskyi.com