diff --git a/.phpstan.dist.baseline.neon b/.phpstan.dist.baseline.neon index 2629281782e..48a3cd27e25 100644 --- a/.phpstan.dist.baseline.neon +++ b/.phpstan.dist.baseline.neon @@ -5496,6 +5496,30 @@ parameters: count: 1 path: app/design/adminhtml/default/default/template/system/config/form/field/array.phtml + - + message: '#^Access to protected property Mage_Adminhtml_Block_System_Config_Form_Field_Csp_Hosts\:\:\$_addAfter\.$#' + identifier: property.protected + count: 4 + path: app/design/adminhtml/default/default/template/system/config/form/field/csp.phtml + + - + message: '#^Access to protected property Mage_Adminhtml_Block_System_Config_Form_Field_Csp_Hosts\:\:\$_addButtonLabel\.$#' + identifier: property.protected + count: 2 + path: app/design/adminhtml/default/default/template/system/config/form/field/csp.phtml + + - + message: '#^Access to protected property Mage_Adminhtml_Block_System_Config_Form_Field_Csp_Hosts\:\:\$_columns\.$#' + identifier: property.protected + count: 5 + path: app/design/adminhtml/default/default/template/system/config/form/field/csp.phtml + + - + message: '#^Call to protected method _renderCellTemplate\(\) of class Mage_Adminhtml_Block_System_Config_Form_Field_Csp_Hosts\.$#' + identifier: method.protected + count: 2 + path: app/design/adminhtml/default/default/template/system/config/form/field/csp.phtml + - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable diff --git a/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Array/Abstract.php b/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Array/Abstract.php index d7027d7f38f..3bd3eebbcf4 100644 --- a/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Array/Abstract.php +++ b/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Array/Abstract.php @@ -17,7 +17,7 @@ abstract class Mage_Adminhtml_Block_System_Config_Form_Field_Array_Abstract exte /** * Grid columns * - * @var array + * @var array */ protected $_columns = []; @@ -38,9 +38,9 @@ abstract class Mage_Adminhtml_Block_System_Config_Form_Field_Array_Abstract exte /** * Rows cache * - * @var array|null + * @var array|null */ - private $_arrayRowsCache; + protected $_arrayRowsCache; /** * Indication whether block is prepared to render or no diff --git a/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Csp/Hosts.php b/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Csp/Hosts.php new file mode 100644 index 00000000000..70aad2dc17f --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Csp/Hosts.php @@ -0,0 +1,152 @@ +helper = $helper; + $this->addColumn('host', [ + 'label' => Mage::helper('csp')->__('Host'), + ]); + + $this->_addAfter = false; + $this->_addButtonLabel = Mage::helper('csp')->__('Add Host'); + $this->setTemplate('system/config/form/field/csp.phtml'); + + parent::__construct(); + } + + /** + * Obtain existing data from form element + * + * Each row will be instance of Varien_Object + * @return array Array of rows + * @throws Exception + */ + public function getArrayRows(): array + { + if ($this->_arrayRowsCache !== null) { + return $this->_arrayRowsCache; + } + + $result = []; + + [$area, $directiveName] = $this->_parseNodePath(); + + $globalPolicy = $this->helper->getGlobalPolicy($directiveName); + if ($globalPolicy) { + foreach ($globalPolicy as $key => $host) { + $rowId = $directiveName . '_xml_' . $area . '_' . $key; + $result[$rowId] = new Varien_Object([ + 'host' => $host, + 'readonly' => 'readonly="readonly"', + '_id' => $rowId, + 'area' => 'global', + ]); + $this->_prepareArrayRow($result[$rowId]); + } + } + + $areaPolicy = $this->helper->getAreaPolicy($area, $directiveName); + if ($areaPolicy) { + foreach ($areaPolicy as $key => $host) { + $rowId = $directiveName . '_xml_' . $area . '_' . $key; + $result[$rowId] = new Varien_Object([ + 'host' => $host, + 'readonly' => 'readonly="readonly"', + '_id' => $rowId, + 'area' => $area, + ]); + $this->_prepareArrayRow($result[$rowId]); + } + } + + $configPolicy = $this->helper->getStoreConfigPolicy($area, $directiveName); + if ($configPolicy) { + foreach ($configPolicy as $key => $value) { + $rowId = $directiveName . '_' . $area . '_' . $key; + $result[$rowId] = new Varien_Object([ + 'host' => $this->escapeHtml($value), + '_id' => $rowId, + ]); + + $this->_prepareArrayRow($result[$rowId]); + } + } + + $this->_arrayRowsCache = $result; + return $this->_arrayRowsCache; + } + + /** + * Extract and validate area and directive name from the node path + * + * @return array{Mage_Core_Model_App_Area::AREA_FRONTEND|Mage_Core_Model_App_Area::AREA_ADMINHTML, value-of} Array containing area and directiveName + * @throws Exception If path format is invalid or contains disallowed values + */ + private function _parseNodePath(): array + { + /** @var Varien_Data_Form_Element_Abstract $element */ + $element = $this->getElement(); + $configPath = $element->getData('config_path'); + + $allowedDirectives = implode('|', Mage_Csp_Helper_Data::CSP_DIRECTIVES); + $allowedAreas = Mage_Core_Model_App_Area::AREA_FRONTEND . '|' . Mage_Core_Model_App_Area::AREA_ADMINHTML; + + $pattern = "#csp/({$allowedAreas})/({$allowedDirectives})#"; + + if (!$configPath || !preg_match($pattern, $configPath, $matches)) { + throw new Exception('Invalid node path format or disallowed area/directive'); + } + + $area = $matches[1]; + $directiveName = $matches[2]; + + return [$area, $directiveName]; + } + + /** + * Render array cell for prototypeJS template + * + * @param string $columnName + * @return string + * @throws Exception + */ + protected function _renderCellTemplate($columnName) + { + if (empty($this->_columns[$columnName])) { + throw new Exception('Wrong column name specified.'); + } + + $column = $this->_columns[$columnName]; + /** @var Varien_Data_Form_Element_Text $element */ + $element = $this->getElement(); + $elementName = $element->getName(); + $inputName = $elementName . '[#{_id}][' . $columnName . ']'; + + return ''; + } +} diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Serialized.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Serialized.php index d3b4d3a4b61..d1cdd7be946 100644 --- a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Serialized.php +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Serialized.php @@ -20,7 +20,7 @@ protected function _afterLoad() if (!is_array($this->getValue())) { $serializedValue = $this->getValue(); $unserializedValue = false; - if (!empty($serializedValue)) { + if (!empty($serializedValue) && is_string($serializedValue)) { try { $unserializedValue = Mage::helper('core/unserializeArray') ->unserialize((string) $serializedValue); diff --git a/app/code/core/Mage/Csp/Block/Adminhtml/Meta.php b/app/code/core/Mage/Csp/Block/Adminhtml/Meta.php new file mode 100644 index 00000000000..016884d056d --- /dev/null +++ b/app/code/core/Mage/Csp/Block/Adminhtml/Meta.php @@ -0,0 +1,20 @@ +, array> + */ + protected array $directives = []; + + /** + * CSP meta tag area + * @var Mage_Core_Model_App_Area::AREA_FRONTEND|Mage_Core_Model_App_Area::AREA_ADMINHTML + */ + protected string $area = Mage_Core_Model_App_Area::AREA_FRONTEND; + + /** + * Add CSP directive + * + * @param value-of $directive + */ + public function addDirective(string $directive, string $value): static + { + if (!in_array($directive, Mage_Csp_Helper_Data::CSP_DIRECTIVES)) { + return $this; + } + + if (!isset($this->directives[$directive])) { + $this->directives[$directive] = []; + } + + $this->directives[$directive][] = $value; + + return $this; + } + + /** + * Get CSP directives + * @return array, array> + */ + public function getDirectives(): array + { + return $this->directives; + } + + /** + * Get CSP policy content + */ + public function getContents(): string + { + $content = []; + foreach ($this->directives as $directive => $values) { + if (!empty($values)) { + $content[] = $directive . ' ' . implode(' ', $values); + } + } + $content = implode('; ', $content); + return trim($content); + } + + /** + * Render CSP meta tag if enabled + */ + protected function _toHtml(): string + { + if (empty($this->directives)) { + return ''; + } + + /** @var Mage_Csp_Helper_Data $helper */ + $helper = Mage::helper('csp'); + if (!$helper->isEnabled($this->area) || $helper->shouldMergeMeta($this->area)) { + return ''; + } + + $headerValue = $this->getContents(); + if (!empty($helper->getReportUri($this->area))) { + $reportUriEndpoint = trim($helper->getReportUri($this->area)); + $headerValue .= '; report-uri ' . $reportUriEndpoint; + } + $headerName = $helper->getReportOnly($this->area) + ? Mage_Csp_Helper_Data::HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY + : Mage_Csp_Helper_Data::HEADER_CONTENT_SECURITY_POLICY; + + return sprintf( + '' . PHP_EOL, + $headerName, + $headerValue, + ); + } +} 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..3b690af5e70 --- /dev/null +++ b/app/code/core/Mage/Csp/Helper/Data.php @@ -0,0 +1,187 @@ +getReportOnly($area) ? self::HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY : self::HEADER_CONTENT_SECURITY_POLICY; + } + + /** + * Get CSP policies for a specific area + * @param Mage_Core_Model_App_Area::AREA_FRONTEND|Mage_Core_Model_App_Area::AREA_ADMINHTML $area + * @return array> + */ + public function getPolicies(string $area = Mage_Core_Model_App_Area::AREA_FRONTEND): array + { + if (!$this->isEnabled($area)) { + return []; + } + $policy = []; + foreach (self::CSP_DIRECTIVES as $directiveName) { + $policy[$directiveName] = []; + $policy[$directiveName] = array_merge_recursive( + $this->getGlobalPolicy($directiveName), + $this->getAreaPolicy($area, $directiveName), + $this->getStoreConfigPolicy($area, $directiveName), + ); + $policy[$directiveName] = array_unique($policy[$directiveName]); + } + + /** @var array> $policy */ + return $policy; + } + + /** + * Get global policy for a specific directive + * global/csp/ + * @param value-of $directiveName + * @return array + */ + public function getGlobalPolicy(string $directiveName = 'default-src'): array + { + $config = Mage::getConfig(); + if (!$config) { + return []; + } + + $globalNode = $config->getNode(sprintf('global/csp/%s', $directiveName)); + if ($globalNode) { + /** @var array $result */ + $result = $globalNode->asArray(); + return $result; + } + return []; + } + + /** + * Get area policy for a specific directive + * (adminhtml|frontend)/csp/ + * @param Mage_Core_Model_App_Area::AREA_FRONTEND|Mage_Core_Model_App_Area::AREA_ADMINHTML $area + * @param value-of $directiveName + * @return array + */ + public function getAreaPolicy(string $area = Mage_Core_Model_App_Area::AREA_FRONTEND, string $directiveName = 'default-src'): array + { + $config = Mage::getConfig(); + if (!$config) { + return []; + } + + $areaNode = $config->getNode(sprintf('%s/csp/%s', $area, $directiveName)); + if ($areaNode) { + /** @var array $result */ + $result = $areaNode->asArray(); + return $result; + } + return []; + } + + + /** + * Get system policy for a specific directive + * csp/(adminhtml|frontend)/ + * @param Mage_Core_Model_App_Area::AREA_FRONTEND|Mage_Core_Model_App_Area::AREA_ADMINHTML $area + * @param value-of $directiveName + * @return array + */ + public function getStoreConfigPolicy(string $area = Mage_Core_Model_App_Area::AREA_FRONTEND, string $directiveName = 'default-src'): array + { + /** @var array|null $systemNode */ + $systemNode = Mage::getStoreConfig(sprintf('csp/%s/%s', $area, $directiveName)); + if ($systemNode) { + if (is_string($systemNode) && preg_match('/^a:\d+:{.*}$/', $systemNode)) { + $unserializedData = unserialize($systemNode); + if (is_array($unserializedData)) { + /** @var array $systemNode */ + $systemNode = array_column($unserializedData, 'host'); + } + } + return $systemNode; + } + return []; + } +} diff --git a/app/code/core/Mage/Csp/Model/Observer/Abstract.php b/app/code/core/Mage/Csp/Model/Observer/Abstract.php new file mode 100644 index 00000000000..dce0af6b2f3 --- /dev/null +++ b/app/code/core/Mage/Csp/Model/Observer/Abstract.php @@ -0,0 +1,97 @@ +getEvent()->getDataByKey('response'); + if (!$response || !$response->canSendHeaders(true)) { + return; + } + + /** @var Mage_Csp_Helper_Data $helper */ + $helper = Mage::helper('csp'); + if (!$helper->isEnabled($area)) { + return; + } + + $directives = $helper->getPolicies($area); + if (empty($directives)) { + return; + } + + // Merge meta directives if needed + if ($helper->shouldMergeMeta($area)) { + $blockCspMeta = Mage::app()->getLayout()->getBlock('csp_meta'); + if ($blockCspMeta instanceof Mage_Csp_Block_Meta) { + $metaDirectives = $blockCspMeta->getDirectives(); + foreach ($metaDirectives as $directive => $values) { + $directives[$directive] = array_unique( + array_merge($directives[$directive] ?? [], $values), + ); + } + } + } + + // Set the CSP Reporting-Endpoints header + $reportUriEndpoint = null; + if (!empty($helper->getReportUri($area))) { + $reportUriEndpoint = trim($helper->getReportUri($area)); + $response->setHeader( + Mage_Csp_Helper_Data::HEADER_CONTENT_SECURITY_POLICY_REPORT_URI, + sprintf('csp-endpoint="%s"', $reportUriEndpoint), + ); + } + + $cspDirectives = $this->_compactHeaders($directives); + // Check if the CSP directives should be split into multiple headers + $shouldSplitHeaders = $helper->shouldSplitHeaders($area); + if ($shouldSplitHeaders !== true) { + $headerValue = implode('; ', $cspDirectives); + $cspDirectives = [$headerValue]; + } + // Set the CSP headers + $headerName = $helper->getReportOnly($area) + ? Mage_Csp_Helper_Data::HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY + : Mage_Csp_Helper_Data::HEADER_CONTENT_SECURITY_POLICY; + foreach ($cspDirectives as $headerValue) { + if ($reportUriEndpoint !== null) { + $headerValue .= '; report-uri ' . $reportUriEndpoint; + $headerValue .= '; report-to csp-endpoint'; + } + $response->setHeader($headerName, $headerValue); + } + } + + /** + * Compact the CSP directives into a single string for each directive + * @param array> $directives + * @return array + */ + private function _compactHeaders(array $directives): array + { + $cspParts = []; + foreach ($directives as $directive => $values) { + if (!empty($values)) { + $cspParts[$directive] = $directive . ' ' . implode(' ', $values); + } + } + return $cspParts; + } +} diff --git a/app/code/core/Mage/Csp/Model/Observer/AddAdminCspHeaders.php b/app/code/core/Mage/Csp/Model/Observer/AddAdminCspHeaders.php new file mode 100644 index 00000000000..d2252a19be4 --- /dev/null +++ b/app/code/core/Mage/Csp/Model/Observer/AddAdminCspHeaders.php @@ -0,0 +1,21 @@ +addCspHeaders($observer, Mage_Core_Model_App_Area::AREA_ADMINHTML); + } +} diff --git a/app/code/core/Mage/Csp/Model/Observer/AddFrontendCspHeaders.php b/app/code/core/Mage/Csp/Model/Observer/AddFrontendCspHeaders.php new file mode 100644 index 00000000000..13a462c910c --- /dev/null +++ b/app/code/core/Mage/Csp/Model/Observer/AddFrontendCspHeaders.php @@ -0,0 +1,21 @@ +addCspHeaders($observer, Mage_Core_Model_App_Area::AREA_FRONTEND); + } +} diff --git a/app/code/core/Mage/Csp/etc/adminhtml.xml b/app/code/core/Mage/Csp/etc/adminhtml.xml new file mode 100644 index 00000000000..23be2a9910d --- /dev/null +++ b/app/code/core/Mage/Csp/etc/adminhtml.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + CSP Protection + 70 + + + + + + + + + + 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..108156f26ff --- /dev/null +++ b/app/code/core/Mage/Csp/etc/config.xml @@ -0,0 +1,170 @@ + + + + + + 1.0.0 + + + + + + Mage_Csp_Block + + + + + Mage_Csp_Helper + + + + + Mage_Csp_Model + + + + + 'self' + + + 'self' + + + 'self' + + + 'self' + data: + + + 'self' + + + 'self' + + + 'self' + + + 'self' + + + 'none' + + + 'self' + + + + + + + + csp.xml + + + + + + + + csp/observer_addFrontendCspHeaders + execute + + + + + + + + + + csp.xml + + + + + + 'unsafe-inline' + + + 'unsafe-inline' + + + + + + + csp/observer_addAdminCspHeaders + execute + + + + + + + + + 1 + 0 + 1 + + '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 + + + 'unsafe-inline' + fonts.googleapis.com + maxcdn.bootstrapcdn.com + + + www.google-analytics.com + stats.g.doubleclick.net + www.paypal.com + www.paypalobjects.com + connect.facebook.net + + + www.google-analytics.com + www.paypal.com + securepayments.paypal.com + api.braintreegateway.com + js.stripe.com + api.stripe.com + + + fonts.gstatic.com + maxcdn.bootstrapcdn.com + + + www.paypal.com + payments.amazon.com + + + www.paypal.com + securepayments.paypal.com + + + + 1 + 0 + 1 + + + + + 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..4b6b0166d62 --- /dev/null +++ b/app/code/core/Mage/Csp/etc/system.xml @@ -0,0 +1,363 @@ + + + + + + + advanced + 300 + 1 + 1 + 1 + + + + 100 + 1 + 1 + 1 + + + + select + adminhtml/system_config_source_yesno + 10 + 1 + 1 + 1 + + + + select + adminhtml/system_config_source_yesno + 11 + 1 + 1 + 1 + + 1 + + + + + text + 12 + 1 + 1 + 1 + + + + + select + adminhtml/system_config_source_yesno + 13 + 1 + 1 + 1 + + 1 + + + + + + select + adminhtml/system_config_source_yesno + 14 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 20 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 21 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 22 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 23 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 24 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 25 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 26 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 27 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 28 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 29 + 1 + 1 + 1 + + 1 + + + + + + + 200 + 1 + + + + select + adminhtml/system_config_source_yesno + 10 + 1 + + + + select + adminhtml/system_config_source_yesno + 11 + 1 + + 1 + + + + + text + 12 + 1 + + + + + select + adminhtml/system_config_source_yesno + 13 + 1 + 1 + 1 + + 1 + + + + + + select + adminhtml/system_config_source_yesno + 14 + 1 + 1 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 20 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 21 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 22 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 23 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 24 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 25 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 26 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 27 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 28 + 1 + + 1 + + + + + adminhtml/system_config_form_field_csp_hosts + adminhtml/system_config_backend_serialized_array + 29 + 1 + + 1 + + + + + + + + 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..01191075087 --- /dev/null +++ b/app/design/adminhtml/default/default/layout/csp.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/design/adminhtml/default/default/template/system/config/form/field/csp.phtml b/app/design/adminhtml/default/default/template/system/config/form/field/csp.phtml new file mode 100644 index 00000000000..a7979c2737f --- /dev/null +++ b/app/design/adminhtml/default/default/template/system/config/form/field/csp.phtml @@ -0,0 +1,187 @@ + +getHtmlId() ? $this->getHtmlId() : '_' . uniqid(); + +$_colspan = 2; +if (!$this->_addAfter) { + $_colspan -= 1; +} +$_colspan = $_colspan > 1 ? 'colspan="' . $_colspan . '"' : ''; +?> + +
+ + + + +_columns as $columnName => $column):?> + + + + + + + + + + + +
>
> + +
+ +
+
+ +
+ + 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..50548ef4321 --- /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 + + + + + +