CVE-2023-2339: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Reflected in GitHub repository pimcore/pimcore prior to 10.5.21.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2023-04-27T12:15Z
Repository: https://github.com/pimcore/pimcore
Language: PHP
Files changed: bundles/AdminBundle/Controller/Admin/Asset/AssetHelperController.php, bundles/AdminBundle/Controller/Admin/DataObject/DataObjectHelperController.php, bundles/AdminBundle/Resources/public/js/pimcore/object/gridcolumn/operator/AnyGetter.js, bundles/AdminBundle/Resources/public/js/pimcore/settings/metadata/predefined.js, lib/DataObject/GridColumnConfig/Operator/AbstractOperator.php, lib/DataObject/GridColumnConfig/Operator/AnyGetter.php, lib/Security/SecurityHelper.php
Commit date: 2023-04-21 10:46:57 +0200
Commit message: [Security] Stored cross site scripting vulnerability in operator any getter in pimcore grid configuration (#14984)

* Fix: xss in anyGetter

* Fix: xss for predefined asset metadata
Diff: diff --git a/bundles/AdminBundle/Controller/Admin/Asset/AssetHelperController.php b/bundles/AdminBundle/Controller/Admin/Asset/AssetHelperController.php
index e7849dc20d01..8701ac111e51 100644
--- a/bundles/AdminBundle/Controller/Admin/Asset/AssetHelperController.php
+++ b/bundles/AdminBundle/Controller/Admin/Asset/AssetHelperController.php
@@ -31,10 +31,11 @@ use Pimcore\Model\Element;
 use Pimcore\Model\GridConfig;
 use Pimcore\Model\GridConfigFavourite;
 use Pimcore\Model\GridConfigShare;
 use Pimcore\Model\Metadata;
 use Pimcore\Model\User;
+use Pimcore\Security\SecurityHelper;
 use Pimcore\Tool;
 use Pimcore\Tool\Storage;
 use Pimcore\Version;
 use Symfony\Component\EventDispatcher\GenericEvent;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
@@ -961,13 +962,14 @@ class AssetHelperController extends AdminController
             //only allow unique metadata columns with subtypes
             $uniqueKey = $item->getName().'_'.$item->getTargetSubtype();
             if (!in_array($uniqueKey, $tmp) && !in_array($item->getName(), $defaultMetadataNames)) {
                 $tmp[] = $uniqueKey;
                 $item->expand();
+                $name = SecurityHelper::convertHtmlSpecialChars($item->getName());
                 $metadataItems[] = [
-                    'title' => $item->getName(),
-                    'name' => $item->getName(),
+                    'title' => $name,
+                    'name' => $name,
                     'subtype' => $item->getTargetSubtype(),
                     'datatype' => 'data',
                     'fieldtype' => $item->getType(),
                     'config' => $item->getConfig(),
                 ];
diff --git a/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectHelperController.php b/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectHelperController.php
index db28959f4d53..6a3ba048b1fa 100644
--- a/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectHelperController.php
+++ b/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectHelperController.php
@@ -30,10 +30,11 @@ use Pimcore\Logger;
 use Pimcore\Model\DataObject;
 use Pimcore\Model\GridConfig;
 use Pimcore\Model\GridConfigFavourite;
 use Pimcore\Model\GridConfigShare;
 use Pimcore\Model\User;
+use Pimcore\Security\SecurityHelper;
 use Pimcore\Tool;
 use Pimcore\Tool\Storage;
 use Pimcore\Version;
 use Symfony\Component\EventDispatcher\GenericEvent;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
@@ -353,10 +354,17 @@ class DataObjectHelperController extends AdminController
                 }
                 $modificationDate = $savedGridConfig->getModificationDate();
                 $gridConfigDescription = $savedGridConfig->getDescription();
                 $sharedGlobally = $savedGridConfig->isShareGlobally();
                 $setAsFavourite = $savedGridConfig->isSetAsFavourite();
+
+                foreach($gridConfig['columns'] as &$column) {
+                    if (array_key_exists('isOperator', $column) && $column['isOperator']) {
+                        $colAttributes = &$column['fieldConfig']['attributes'];
+                        SecurityHelper::convertHtmlSpecialCharsArrayKeys($colAttributes, ['label', 'attribute', 'param1']);
+                    }
+                }
             }
         }
 
         $localizedFields = [];
         $objectbrickFields = [];
diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/object/gridcolumn/operator/AnyGetter.js b/bundles/AdminBundle/Resources/public/js/pimcore/object/gridcolumn/operator/AnyGetter.js
index 3ee3c4bff73f..274279575d94 100644
--- a/bundles/AdminBundle/Resources/public/js/pimcore/object/gridcolumn/operator/AnyGetter.js
+++ b/bundles/AdminBundle/Resources/public/js/pimcore/object/gridcolumn/operator/AnyGetter.js
@@ -81,26 +81,29 @@ pimcore.object.gridcolumn.operator.anygetter = Class.create(pimcore.object.gridc
 
             this.textfield = new Ext.form.TextField({
                 fieldLabel: t('label'),
                 length: 255,
                 width: 200,
-                value: this.node.data.configAttributes.label
+                value: this.node.data.configAttributes.label,
+                listeners: {'change': pimcore.helpers.htmlEncodeTextField }
             });
 
             this.attributeField = new Ext.form.TextField({
                 fieldLabel: t('attribute'),
                 length: 255,
                 width: 200,
-                value: this.node.data.configAttributes.attribute
+                value: this.node.data.configAttributes.attribute,
+                listeners: {'change': pimcore.helpers.htmlEncodeTextField }
             });
 
             this.param1Field = new Ext.form.TextField({
                 fieldLabel: t('parameter'),
                 length: 255,
                 width: 200,
-                value: this.node.data.configAttributes.param1
-            });
+                value: this.node.data.configAttributes.param1,
+                listeners: {'change': pimcore.helpers.htmlEncodeTextField }
+        });
 
             this.returnLastResultField = new Ext.form.Checkbox({
                 fieldLabel: t('return_last_result'),
                 length: 255,
                 width: 200,
@@ -181,11 +184,11 @@ pimcore.object.gridcolumn.operator.anygetter = Class.create(pimcore.object.gridc
             if (configAttributes.attribute) {
                 var attr = configAttributes.attribute;
                 if (configAttributes.param1) {
                     attr += " " + configAttributes.param1;
                 }
-                nodeLabel += '<span class="pimcore_gridnode_hint"> (' + attr + ')</span>';
+                nodeLabel += '<span class="pimcore_gridnode_hint"> (' + Ext.util.Format.htmlEncode(attr) + ')</span>';
             }
 
             return nodeLabel;
         }
     }
diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/settings/metadata/predefined.js b/bundles/AdminBundle/Resources/public/js/pimcore/settings/metadata/predefined.js
index a7e644ae987b..d9f2fa9d8fd0 100644
--- a/bundles/AdminBundle/Resources/public/js/pimcore/settings/metadata/predefined.js
+++ b/bundles/AdminBundle/Resources/public/js/pimcore/settings/metadata/predefined.js
@@ -136,11 +136,11 @@ pimcore.settings.metadata.predefined = Class.create({
                 width: 40,
                 renderer: this.getTypeRenderer.bind(this),
                 sortable: true
             },
             {text: t("name"), width: 200, sortable: true, dataIndex: 'name',
-                getEditor: function() { return new Ext.form.TextField({}); }
+                getEditor: function() { return new Ext.form.TextField({ listeners: {'change': pimcore.helpers.htmlEncodeTextField } }); }
             },
             {text: t("group"), width: 200, sortable: true, dataIndex: 'group',
                 getEditor: function() { return new Ext.form.TextField({}); }
             },
             {text: t("description"), sortable: true, dataIndex: 'description',
diff --git a/lib/DataObject/GridColumnConfig/Operator/AbstractOperator.php b/lib/DataObject/GridColumnConfig/Operator/AbstractOperator.php
index 47e5545d27a1..783170c7c4ce 100644
--- a/lib/DataObject/GridColumnConfig/Operator/AbstractOperator.php
+++ b/lib/DataObject/GridColumnConfig/Operator/AbstractOperator.php
@@ -14,10 +14,11 @@
  */
 
 namespace Pimcore\DataObject\GridColumnConfig\Operator;
 
 use Pimcore\DataObject\GridColumnConfig\ConfigElementInterface;
+use Pimcore\Security\SecurityHelper;
 use Pimcore\Tool;
 
 abstract class AbstractOperator implements OperatorInterface
 {
     /**
@@ -39,11 +40,11 @@ abstract class AbstractOperator implements OperatorInterface
      * @param \stdClass $config
      * @param array $context
      */
     public function __construct(\stdClass $config, array $context = [])
     {
-        $this->label = $config->label;
+        $this->label = SecurityHelper::convertHtmlSpecialChars($config->label);
         $this->childs = $config->childs;
         $this->context = $context;
     }
 
     /**
@@ -89,11 +90,11 @@ abstract class AbstractOperator implements OperatorInterface
     /**
      * @param string $label
      */
     public function setLabel($label)
     {
-        $this->label = $label;
+        $this->label = SecurityHelper::convertHtmlSpecialChars($label);
     }
 
     /**
      * @return string[]
      */
diff --git a/lib/DataObject/GridColumnConfig/Operator/AnyGetter.php b/lib/DataObject/GridColumnConfig/Operator/AnyGetter.php
index ccf970ce46cb..6bd0e9ef663c 100644
--- a/lib/DataObject/GridColumnConfig/Operator/AnyGetter.php
+++ b/lib/DataObject/GridColumnConfig/Operator/AnyGetter.php
@@ -14,10 +14,11 @@
  */
 
 namespace Pimcore\DataObject\GridColumnConfig\Operator;
 
 use Pimcore\Model\AbstractModel;
+use Pimcore\Security\SecurityHelper;
 use Pimcore\Tool\Admin;
 
 /**
  * @internal
  */
@@ -62,12 +63,12 @@ final class AnyGetter extends AbstractOperator
             throw new \Exception('AnyGetter only allowed for admin users');
         }
 
         parent::__construct($config, $context);
 
-        $this->attribute = $config->attribute ?? '';
-        $this->param1 = $config->param1 ?? '';
+        $this->attribute = SecurityHelper::convertHtmlSpecialChars($config->attribute ?? '');
+        $this->param1 = SecurityHelper::convertHtmlSpecialChars($config->param1 ?? '');
         $this->isArrayType = $config->isArrayType ?? false;
 
         $this->forwardAttribute = $config->forwardAttribute ?? '';
         $this->forwardParam1 = $config->forwardParam1 ?? '';
 
@@ -180,11 +181,11 @@ final class AnyGetter extends AbstractOperator
     /**
      * @param string $attribute
      */
     public function setAttribute($attribute)
     {
-        $this->attribute = $attribute;
+        $this->attribute = SecurityHelper::convertHtmlSpecialChars($attribute);
     }
 
     /**
      * @return string
      */
@@ -196,11 +197,11 @@ final class AnyGetter extends AbstractOperator
     /**
      * @param string $param1
      */
     public function setParam1($param1)
     {
-        $this->param1 = $param1;
+        $this->param1 = SecurityHelper::convertHtmlSpecialChars($param1);
     }
 
     /**
      * @return string
      */
diff --git a/lib/Security/SecurityHelper.php b/lib/Security/SecurityHelper.php
index 541238e2bbaf..4ba989380af7 100644
--- a/lib/Security/SecurityHelper.php
+++ b/lib/Security/SecurityHelper.php
@@ -23,6 +23,15 @@ class SecurityHelper
             return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false);
         }
 
         return null;
     }
+
+    public static function convertHtmlSpecialCharsArrayKeys(array &$array, array $keys): void
+    {
+        foreach ($keys as $key) {
+            if (array_key_exists($key, $array)) {
+                $array[$key] = self::convertHtmlSpecialChars($array[$key]);
+            }
+        }
+    }
 }

Vulnerable code:                     'title' => $item->getName(),
                    'name' => $item->getName(),
                value: this.node.data.configAttributes.label
                value: this.node.data.configAttributes.attribute
                value: this.node.data.configAttributes.param1
            });
                nodeLabel += '<span class="pimcore_gridnode_hint"> (' + attr + ')</span>';
                getEditor: function() { return new Ext.form.TextField({}); }
        $this->label = $config->label;
        $this->label = $label;
        $this->attribute = $config->attribute ?? '';
        $this->param1 = $config->param1 ?? '';
        $this->attribute = $attribute;
        $this->param1 = $param1;
Fixed code: use Pimcore\Security\SecurityHelper;
                $name = SecurityHelper::convertHtmlSpecialChars($item->getName());
                    'title' => $name,
                    'name' => $name,
use Pimcore\Security\SecurityHelper;

                foreach($gridConfig['columns'] as &$column) {
                    if (array_key_exists('isOperator', $column) && $column['isOperator']) {
                        $colAttributes = &$column['fieldConfig']['attributes'];
                        SecurityHelper::convertHtmlSpecialCharsArrayKeys($colAttributes, ['label', 'attribute', 'param1']);
                    }
                }
                value: this.node.data.configAttributes.label,
                listeners: {'change': pimcore.helpers.htmlEncodeTextField }
                value: this.node.data.configAttributes.attribute,
                listeners: {'change': pimcore.helpers.htmlEncodeTextField }
                value: this.node.data.configAttributes.param1,
                listeners: {'change': pimcore.helpers.htmlEncodeTextField }
        });
                nodeLabel += '<span class="pimcore_gridnode_hint"> (' + Ext.util.Format.htmlEncode(attr) + ')</span>';
                getEditor: function() { return new Ext.form.TextField({ listeners: {'change': pimcore.helpers.htmlEncodeTextField } }); }
use Pimcore\Security\SecurityHelper;
        $this->label = SecurityHelper::convertHtmlSpecialChars($config->label);
        $this->label = SecurityHelper::convertHtmlSpecialChars($label);
use Pimcore\Security\SecurityHelper;
        $this->attribute = SecurityHelper::convertHtmlSpecialChars($config->attribute ?? '');
        $this->param1 = SecurityHelper::convertHtmlSpecialChars($config->param1 ?? '');
        $this->attribute = SecurityHelper::convertHtmlSpecialChars($attribute);
        $this->param1 = SecurityHelper::convertHtmlSpecialChars($param1);

    public static function convertHtmlSpecialCharsArrayKeys(array &$array, array $keys): void
    {
        foreach ($keys as $key) {
            if (array_key_exists($key, $array)) {
                $array[$key] = self::convertHtmlSpecialChars($array[$key]);
            }
        }
    }