diff --git a/.phpstan.dist.baseline.neon b/.phpstan.dist.baseline.neon
index 93ccdb6cdc0..80461ba89d4 100644
--- a/.phpstan.dist.baseline.neon
+++ b/.phpstan.dist.baseline.neon
@@ -8621,3 +8621,8 @@ parameters:
identifier: greaterOrEqual.alwaysTrue
count: 1
path: tests/unit/Mage/Core/Model/LayoutTest.php
+ -
+ message: '#^Return type \(Zend_Cache_Core\) of method Mage_Csp_Model_Config\:\:getCache\(\) should be compatible with return type \(Varien_Simplexml_Config_Cache_Abstract\) of method Varien_Simplexml_Config\:\:getCache\(\)$#'
+ identifier: method.childReturnType
+ count: 1
+ path: app/code/core/Mage/Csp/Model/Config.php
diff --git a/app/code/core/Mage/Csp/Block/Adminhtml/Csp.php b/app/code/core/Mage/Csp/Block/Adminhtml/Csp.php
new file mode 100644
index 00000000000..05c5e9fa84f
--- /dev/null
+++ b/app/code/core/Mage/Csp/Block/Adminhtml/Csp.php
@@ -0,0 +1,21 @@
+> */
+ protected array $items = [];
+ protected string $section = 'system';
+
+ public function addItem(string $type, string $data): self
+ {
+ $this->items[$type][] = $data;
+ return $this;
+ }
+
+ /**
+ * @throws Zend_Controller_Response_Exception
+ */
+ protected function _toHtml(): string
+ {
+ $response = $this->getAction()->getResponse();
+ if (!$response->canSendHeaders()) {
+ return '';
+ }
+
+ /** @var Mage_Csp_Helper_Data $helper */
+ $helper = Mage::helper('csp');
+
+ if (!$helper->isCspEnabled($this->section)) {
+ return '';
+ }
+
+ /** @var Mage_Csp_Model_Config $config */
+ $config = Mage::getSingleton('csp/config');
+ $directives = array_merge_recursive(
+ $helper->getPolicies($this->section),
+ $config->getPolicies(),
+ $this->items,
+ );
+ $cspHeader = [];
+ foreach ($directives as $directive => $value) {
+ $cspHeader[] = $directive . ' ' . (is_array($value) ? implode(' ', $value) : (string) $value);
+ }
+
+ $header = $helper->getCspHeader($this->section);
+ $response->setHeader($header, implode('; ', $cspHeader));
+ return '';
+ }
+}
diff --git a/app/code/core/Mage/Csp/Helper/Data.php b/app/code/core/Mage/Csp/Helper/Data.php
new file mode 100644
index 00000000000..d7f5a489a90
--- /dev/null
+++ b/app/code/core/Mage/Csp/Helper/Data.php
@@ -0,0 +1,72 @@
+
+ */
+ public function getPolicies(string $section): array
+ {
+ $result = [];
+
+ if (!$this->isCspEnabled($section)) {
+ return $result;
+ }
+
+ foreach (self::CONFIG_MAPPING as $key) {
+ $result[$key] = $this->getCspConfigByKey($section, $key);
+ }
+ return $result;
+ }
+
+ public function isCspEnabled(string $section): bool
+ {
+ return Mage::getStoreConfigFlag("$section/csp/enabled");
+ }
+
+ public function isCspReportOnly(string $section): bool
+ {
+ return Mage::getStoreConfigFlag("$section/csp/report_only");
+ }
+
+ public function getCspConfigByKey(string $section, string $key): string
+ {
+ return Mage::getStoreConfig("$section/csp/$key");
+ }
+
+ public function getCspHeader(string $section): string
+ {
+ return $this->isCspReportOnly($section) ?
+ 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
+ }
+}
diff --git a/app/code/core/Mage/Csp/Model/Config.php b/app/code/core/Mage/Csp/Model/Config.php
new file mode 100644
index 00000000000..546b10cff75
--- /dev/null
+++ b/app/code/core/Mage/Csp/Model/Config.php
@@ -0,0 +1,166 @@
+setCacheId(self::CACHE_ID);
+ $this->setCacheTags([self::CACHE_TAG]);
+ $this->setCacheChecksum(null);
+
+ parent::__construct($sourceData);
+
+ $this->_construct();
+ }
+
+ /**
+ * Init configuration for webservices api
+ *
+ * @return $this
+ */
+ protected function _construct(): self
+ {
+ if ($this->hasUseCache() && $this->loadCache()) {
+ return $this;
+ }
+
+ $this->loadString('');
+ $config = Mage::getConfig()->loadModulesConfiguration('csp.xml', $this);
+
+ $node = $config->getNode();
+ if ($node) {
+ $this->setXml($node);
+ }
+
+ if ($this->hasUseCache()) {
+ $this->saveCache();
+ }
+ return $this;
+ }
+
+ /**
+ * Retrieve all adapters
+ * @return array>
+ */
+ public function getPolicies(): array
+ {
+ $policies = [];
+
+ $xpaths = $this->getXpath('csp/policy');
+ if (!$xpaths) {
+ return $policies;
+ }
+
+ foreach ($xpaths as $config) {
+ foreach ($config as $policy => $rules) {
+ foreach ($rules as $host) {
+ $policies[$policy][] = (string) $host;
+ }
+ }
+ }
+
+ return $policies;
+ }
+
+ /**
+ * Retrieve cache object
+ *
+ * @return Zend_Cache_Core
+ */
+ public function getCache()
+ {
+ return Mage::app()->getCache();
+ }
+
+ /**
+ * @param string $id
+ */
+ protected function _loadCache($id): bool
+ {
+ return (bool) Mage::app()->loadCache($id);
+ }
+
+ /**
+ * @param string $data
+ * @param string $id
+ * @param array $tags
+ * @param false|int $lifetime
+ */
+ protected function _saveCache($data, $id, $tags = [], $lifetime = false): bool
+ {
+ Mage::app()->saveCache($data, $id, $tags, $lifetime);
+ return true;
+ }
+
+ /**
+ * @param string $id
+ */
+ protected function _removeCache($id): void
+ {
+ Mage::app()->removeCache($id);
+ }
+
+ /**
+ * @param bool $overwrite
+ * @return $this
+ */
+ public function extend(Varien_Simplexml_Config $config, $overwrite = false): self
+ {
+ $config = $config->getNode();
+
+ if (!$config instanceof Varien_Simplexml_Element) {
+ return $this;
+ }
+
+ $node = $this->getNode();
+ if ($node) {
+ $this->_extendNode($node, $config, $overwrite);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Custom merging logic that preserves duplicate nodes.
+ */
+ protected function _extendNode(Varien_Simplexml_Element $baseNode, Varien_Simplexml_Element $mergeNode, bool $overwrite = false): void
+ {
+ foreach ($mergeNode->children() as $key => $child) {
+ $newChild = $baseNode->addChild($key, (string) $child);
+ foreach ($child->attributes() as $attrKey => $attrValue) {
+ $newChild->addAttribute($attrKey, (string) $attrValue);
+ }
+ $this->_extendNode($newChild, $child, $overwrite);
+ }
+ }
+
+ /**
+ * @return array|bool|null
+ */
+ protected function hasUseCache(): array|bool|null
+ {
+ return Mage::app()->useCache(self::CACHE_TYPE);
+ }
+}
diff --git a/app/code/core/Mage/Csp/etc/config.xml b/app/code/core/Mage/Csp/etc/config.xml
new file mode 100644
index 00000000000..97bf32b0f06
--- /dev/null
+++ b/app/code/core/Mage/Csp/etc/config.xml
@@ -0,0 +1,60 @@
+
+
+
+
+ 1.0.0
+
+
+
+
+
+ Mage_Csp_Block
+
+
+
+
+ Mage_Csp_Helper
+
+
+
+
+ Mage_Csp_Model
+
+
+
+
+
+
+
+ csp.xml
+
+
+
+
+
+
+
+
+ csp.xml
+
+
+
+
+
+
+
+ 'self'
+ 'self' 'unsafe-inline' 'unsafe-eval' www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net www.paypal.com www.paypalobjects.com js.stripe.com connect.facebook.net
+ 'self' 'unsafe-inline' fonts.googleapis.com maxcdn.bootstrapcdn.com,
+ 'self' data: www.google-analytics.com stats.g.doubleclick.net www.paypal.com www.paypalobjects.com connect.facebook.net
+ 'self' www.google-analytics.com www.paypal.com securepayments.paypal.com api.braintreegateway.com js.stripe.com api.stripe.com
+ 'self' fonts.gstatic.com maxcdn.bootstrapcdn.com
+ 'self' www.paypal.com payments.amazon.com
+ 'none'
+ 'self'
+ 'self' www.paypal.com securepayments.paypal.com
+
+
+
+
+
diff --git a/app/code/core/Mage/Csp/etc/csp.xml b/app/code/core/Mage/Csp/etc/csp.xml
new file mode 100644
index 00000000000..8317c3eeeeb
--- /dev/null
+++ b/app/code/core/Mage/Csp/etc/csp.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ *.cloudflare.com
+
+
+
diff --git a/app/code/core/Mage/Csp/etc/system.xml b/app/code/core/Mage/Csp/etc/system.xml
new file mode 100644
index 00000000000..470eaa04bce
--- /dev/null
+++ b/app/code/core/Mage/Csp/etc/system.xml
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
+
+ 300
+ 1
+ 1
+ 1
+
+
+
+ select
+ adminhtml/system_config_source_yesno
+ 10
+ 1
+ 1
+ 1
+
+
+
+
+ text
+ 20
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 21
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 22
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 23
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 24
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 25
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 26
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 27
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 28
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 29
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+
+ select
+ adminhtml/system_config_source_yesno
+ 100
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ 300
+ 1
+ 1
+ 1
+
+
+
+ select
+ adminhtml/system_config_source_yesno
+ 10
+ 1
+ 1
+ 1
+
+
+
+
+ text
+ 20
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 21
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 22
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 23
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 24
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 25
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 26
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 27
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 28
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+ text
+ 29
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+
+ select
+ adminhtml/system_config_source_yesno
+ 100
+ 1
+ 1
+ 1
+
+ 1
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/design/adminhtml/default/default/layout/csp.xml b/app/design/adminhtml/default/default/layout/csp.xml
new file mode 100644
index 00000000000..f98a17a1963
--- /dev/null
+++ b/app/design/adminhtml/default/default/layout/csp.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/design/frontend/base/default/layout/csp.xml b/app/design/frontend/base/default/layout/csp.xml
new file mode 100644
index 00000000000..dbd54d987e3
--- /dev/null
+++ b/app/design/frontend/base/default/layout/csp.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/etc/modules/Mage_Csp.xml b/app/etc/modules/Mage_Csp.xml
new file mode 100644
index 00000000000..156e16d2183
--- /dev/null
+++ b/app/etc/modules/Mage_Csp.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ true
+ core
+
+
+
+
+
+