Skip to content

Content Security Policy (CSP) Implementation a new approach #4776

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

Merged
merged 50 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
96dff68
wip
empiricompany Apr 23, 2025
9b27a60
remove config_csp
empiricompany Apr 23, 2025
9a48276
docs: add workaround for prototypeJS bug with disabled inputs in arra…
empiricompany Apr 23, 2025
5789fa2
allowing for specific directive handling admin / frontend
empiricompany Apr 24, 2025
e9cb09a
fix: serialized backend xml load default
empiricompany Apr 24, 2025
2373a84
Add Content Security Policy (CSP) support directives global, adminhtm…
empiricompany Apr 24, 2025
ff834f2
feat: add report-uri support
empiricompany Apr 24, 2025
fc47226
system config admin only global
empiricompany Apr 24, 2025
d8ff1bc
fix: phpcs
empiricompany Apr 24, 2025
83616f5
make $_arrayRowsCache protected: used by public getArrayRows()
empiricompany Apr 24, 2025
69e6844
feat: add area config info to inputs
empiricompany Apr 25, 2025
81b4c44
chroe: phpstan ignore
empiricompany Apr 25, 2025
be8a64c
chore: phpstan ignore
empiricompany Apr 25, 2025
1b15677
chore: rector
empiricompany Apr 25, 2025
e9490c5
chore
empiricompany Apr 25, 2025
9baf274
chore
empiricompany Apr 25, 2025
6c5fae0
use Reporting-Endpoints for report URI
empiricompany Apr 25, 2025
904e782
trim report uri
empiricompany Apr 25, 2025
b56c732
report uri not dependent fro report_only mode
empiricompany Apr 25, 2025
9a9cb23
add support for <meta> directives
empiricompany Apr 25, 2025
970c2e1
Apply @sreichel suggestions: improve method docs and type hints
empiricompany Apr 27, 2025
da96802
fix suggestion
empiricompany Apr 27, 2025
b9d0c28
fix suggestion
empiricompany Apr 27, 2025
06f3704
docs and type hints
empiricompany Apr 27, 2025
5f52bab
Merge branch 'main' into Mage_Csp
empiricompany Apr 27, 2025
ba5f5f9
Merge branch 'main' into Mage_Csp
empiricompany Apr 28, 2025
1c603ae
Merge branch 'main' into Mage_Csp
empiricompany Apr 30, 2025
60f964d
Merge branch 'main' into Mage_Csp
sreichel May 6, 2025
581ff4c
Merge branch 'main' into Mage_Csp
empiricompany May 8, 2025
9b8a833
feat: add support to split headers for each directive
empiricompany May 8, 2025
15942e7
Merge branch 'Mage_Csp' of https://github.com/empiricompany/openmage …
empiricompany May 8, 2025
4de6d40
fix: disable split headers in frontend CSP configuration by default
empiricompany May 8, 2025
243042a
php-cs-fixer
empiricompany May 8, 2025
6db3a01
Unify Csp Hosts
Hanmac May 8, 2025
eb2be42
~ check CS Fixer
Hanmac May 8, 2025
b17aba6
~ fix abtract
Hanmac May 8, 2025
5b7fbb9
refactor: extract node path parsing logic into a separate method
empiricompany May 8, 2025
e6599ba
fix: remove unnecessary whitespace in _parseNodePath method
empiricompany May 8, 2025
3e28c94
refactor: use short array syntax for node path extraction
empiricompany May 8, 2025
1cc35b2
~ use config path instead of NodePath
Hanmac May 8, 2025
ba487dc
refactor: update CSP classes and methods for improved structure and c…
empiricompany May 9, 2025
1876e80
refactor: remove unused renderer logic in _renderCellTemplate method
empiricompany May 9, 2025
f33f2f6
feat: add support for merging CSP <meta /> directives into HTTP headers
empiricompany May 9, 2025
80f117c
docs: fix correct return type annotation in getDirectives method
empiricompany May 9, 2025
0869014
Update app/code/core/Mage/Csp/Model/Observer/Abstract.php
empiricompany May 10, 2025
c9d7291
Merge branch 'main' into Mage_Csp
empiricompany May 10, 2025
2a5700f
Merge branch 'Mage_Csp' into Mage_Csp
empiricompany May 10, 2025
884a714
Merge pull request #2 from Hanmac/Mage_Csp
empiricompany May 10, 2025
f3ee438
Merge branch 'main' into Mage_Csp
empiricompany May 13, 2025
6572040
Merge branch 'main' into Mage_Csp
empiricompany Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .phpstan.dist.baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ abstract class Mage_Adminhtml_Block_System_Config_Form_Field_Array_Abstract exte
/**
* Grid columns
*
* @var array
* @var array<string, array{label: string, size: string|false, style: ?string, class: ?string, renderer: Mage_Core_Block_Abstract|false}>
*/
protected $_columns = [];

Expand All @@ -38,9 +38,9 @@ abstract class Mage_Adminhtml_Block_System_Config_Form_Field_Array_Abstract exte
/**
* Rows cache
*
* @var array|null
* @var array<string, Varien_Object>|null
*/
private $_arrayRowsCache;
protected $_arrayRowsCache;

/**
* Indication whether block is prepared to render or no
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

/**
* @copyright For copyright and license information, read the COPYING.txt file.
* @link /COPYING.txt
* @license Open Software License (OSL 3.0)
* @package Mage_Csp
*/

/**
* Base class for CSP hosts field renderer
*/
class Mage_Adminhtml_Block_System_Config_Form_Field_Csp_Hosts extends Mage_Adminhtml_Block_System_Config_Form_Field_Array_Abstract
{
protected Mage_Csp_Helper_Data $helper;

/**
* Constructor
*/
public function __construct()
{
/** @var Mage_Csp_Helper_Data $helper */
$helper = Mage::helper('csp');
$this->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<string, Varien_Object> 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<Mage_Csp_Helper_Data::CSP_DIRECTIVES>} 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 '<input type="text" name="' . $inputName . '" value="#{' . $columnName . '}" ' .
'#{readonly}' .
($column['size'] ? 'size="' . $column['size'] . '"' : '') . ' class="' .
($column['class'] ?? 'input-text') . '"' .
(isset($column['style']) ? ' style="' . $column['style'] . '"' : '') . '/>';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions app/code/core/Mage/Csp/Block/Adminhtml/Meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* @copyright For copyright and license information, read the COPYING.txt file.
* @link /COPYING.txt
* @license Open Software License (OSL 3.0)
* @package Mage_Csp
*/

/**
* CSP Meta Block
*
* @package Mage_Csp
*/
class Mage_Csp_Block_Adminhtml_Meta extends Mage_Csp_Block_Meta
{
protected string $area = Mage_Core_Model_App_Area::AREA_ADMINHTML;
}
105 changes: 105 additions & 0 deletions app/code/core/Mage/Csp/Block/Meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

/**
* @copyright For copyright and license information, read the COPYING.txt file.
* @link /COPYING.txt
* @license Open Software License (OSL 3.0)
* @package Mage_Csp
*/

/**
* CSP Meta Block
*
* @package Mage_Csp
*/
class Mage_Csp_Block_Meta extends Mage_Core_Block_Template
{
/**
* CSP directives
* @var array<value-of<Mage_Csp_Helper_Data::CSP_DIRECTIVES>, array<string>>
*/
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<Mage_Csp_Helper_Data::CSP_DIRECTIVES> $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<value-of<Mage_Csp_Helper_Data::CSP_DIRECTIVES>, array<string>>
*/
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(
'<meta http-equiv="%s" content="%s" />' . PHP_EOL,
$headerName,
$headerValue,
);
}
}
Loading
Loading