hryvinskyi / magento2-csp
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
Requires
- php: >=8.1 <8.6
- ext-dom: *
- ext-intl: *
- ext-json: *
- ext-libxml: *
- ext-mbstring: *
- hryvinskyi/magento2-base: ^2.1.6
- hryvinskyi/magento2-logger: ^1.1
- laminas/laminas-http: ^2.15
- laminas/laminas-loader: ^2.8
- magento/framework: ^103.0.6
- magento/module-authorization: ^100.4.6
- magento/module-backend: ^102.0.6
- magento/module-cms: ^104.0.6
- magento/module-config: ^101.2.6
- magento/module-csp: ^100.4.5
- magento/module-email: ^101.1.6
- magento/module-store: ^101.1.6
- magento/module-ui: ^101.2.6
- psr/log: ^1.1 || ^2.0 || ^3.0
- symfony/console: ^5.4 || ^6.0 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^9.0 || ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-15 23:32:41 UTC
README
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-uriformat 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,
*anddata:/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,libxmlandmbstringextensions
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