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 + + + + + +