Skip to content

Add Mage_Csp Module #4753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .phpstan.dist.baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions app/code/core/Mage/Csp/Block/Adminhtml/Csp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

/**
* OpenMage
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available at https://opensource.org/license/osl-3-0-php
*
* @category Mage
* @package Mage_Csp
* @copyright Copyright (c) 2025 The OpenMage Contributors (https://www.openmage.org)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Mage_Csp_Block_Adminhtml_Csp extends Mage_Csp_Block_Csp
{
protected string $section = 'admin';
}
61 changes: 61 additions & 0 deletions app/code/core/Mage/Csp/Block/Csp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/**
* OpenMage
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available at https://opensource.org/license/osl-3-0-php
*
* @category Mage
* @package Mage_Csp
* @copyright Copyright (c) 2025 The OpenMage Contributors (https://www.openmage.org)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Mage_Csp_Block_Csp extends Mage_Core_Block_Abstract
{
/** @var array<string, array<int, string>> */
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 '';
}
}
72 changes: 72 additions & 0 deletions app/code/core/Mage/Csp/Helper/Data.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

/**
* OpenMage
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available at https://opensource.org/license/osl-3-0-php
*
* @category Mage
* @package Mage_Csp
* @copyright Copyright (c) 2025 The OpenMage Contributors (https://www.openmage.org)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Mage_Csp_Helper_Data extends Mage_Core_Helper_Abstract
{
protected $_moduleName = 'Mage_Csp';

public const CONFIG_MAPPING = [
'default-src',
'script-src',
'style-src',
'img-src',
'connect-src',
'font-src',
'frame-src',
'object-src',
'media-src',
'form-action',
];

/**
* @return array<string, string>
*/
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';
}
}
166 changes: 166 additions & 0 deletions app/code/core/Mage/Csp/Model/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

/**
* OpenMage
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available at https://opensource.org/license/osl-3-0-php
*
* @category Mage
* @package Mage_Csp
* @copyright Copyright (c) 2025 The OpenMage Contributors (https://www.openmage.org)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Mage_Csp_Model_Config extends Varien_Simplexml_Config
{
public const CACHE_TYPE = 'config';
public const CACHE_ID = 'config_csp';
public const CACHE_TAG = 'config_csp';

/**
* @inheritDoc
*/
public function __construct($sourceData = null)
{
$this->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/>');
$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<string, array<int, string>>
*/
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<int, string> $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<mixed>|bool|null
*/
protected function hasUseCache(): array|bool|null
{
return Mage::app()->useCache(self::CACHE_TYPE);
}
}
60 changes: 60 additions & 0 deletions app/code/core/Mage/Csp/etc/config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0"?>
<config>
<modules>
<Mage_Csp>
<version>1.0.0</version>
</Mage_Csp>
</modules>
<global>
<blocks>
<csp>
<class>Mage_Csp_Block</class>
</csp>
</blocks>
<helpers>
<csp>
<class>Mage_Csp_Helper</class>
</csp>
</helpers>
<models>
<csp>
<class>Mage_Csp_Model</class>
</csp>
</models>
</global>
<adminhtml>
<layout>
<updates>
<csp>
<file>csp.xml</file>
</csp>
</updates>
</layout>
</adminhtml>
<frontend>
<layout>
<updates>
<csp>
<file>csp.xml</file>
</csp>
</updates>
</layout>
</frontend>
<default>
<system>
<csp>
<default-src>'self'</default-src>
<script-src>'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</script-src>
<style-src>'self' 'unsafe-inline' fonts.googleapis.com maxcdn.bootstrapcdn.com</style-src>,
<img-src>'self' data: www.google-analytics.com stats.g.doubleclick.net www.paypal.com www.paypalobjects.com connect.facebook.net</img-src>
<connect-src>'self' www.google-analytics.com www.paypal.com securepayments.paypal.com api.braintreegateway.com js.stripe.com api.stripe.com</connect-src>
<font-src>'self' fonts.gstatic.com maxcdn.bootstrapcdn.com</font-src>
<frame-src>'self' www.paypal.com payments.amazon.com</frame-src>
<object-src>'none'</object-src>
<media-src>'self'</media-src>
<form-action>'self' www.paypal.com securepayments.paypal.com</form-action>
</csp>
</system>
</default>
</config>

Loading
Loading