CVE-2023-2343: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - DOM 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-27T14:15Z
Repository: https://github.com/pimcore/pimcore
Language: JavaScript
Files changed: bundles/AdminBundle/Controller/Admin/DataObject/ClassificationstoreController.php, bundles/AdminBundle/Resources/public/js/pimcore/helpers.js, bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/collectionsPanel.js, bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/groupsPanel.js, bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/propertiesPanel.js, lib/Security/SecurityHelper.php
Commit date: 2023-04-18 11:56:16 +0200
Commit message: [Security] XSS in Classification Store of Data Objects module (#14933)

* Fix: xss in CS

* Fix: implement feedback
Diff: diff --git a/bundles/AdminBundle/Controller/Admin/DataObject/ClassificationstoreController.php b/bundles/AdminBundle/Controller/Admin/DataObject/ClassificationstoreController.php
index 40fba35d6f08..6d29374d8a8f 100644
--- a/bundles/AdminBundle/Controller/Admin/DataObject/ClassificationstoreController.php
+++ b/bundles/AdminBundle/Controller/Admin/DataObject/ClassificationstoreController.php
@@ -22,10 +22,11 @@ use Pimcore\Model\DataObject;
 use Pimcore\Model\DataObject\ClassDefinition\Data\LayoutDefinitionEnrichmentInterface;
 use Pimcore\Model\DataObject\Classificationstore;
 use Pimcore\Model\Translation;
 use Pimcore\Model\Translation\Listing;
 use Pimcore\Model\User;
+use Pimcore\Security\SecurityHelper;
 use Pimcore\Tool\Admin;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Event\ControllerEvent;
 use Symfony\Component\Routing\Annotation\Route;
@@ -125,11 +126,11 @@ class ClassificationstoreController extends AdminController implements KernelCon
      *
      * @throws \Exception
      */
     public function createGroupAction(Request $request): JsonResponse
     {
-        $name = $request->get('name');
+        $name = SecurityHelper::getStringWithoutControlChars($request->get('name'));
         $storeId = $request->get('storeId');
         $config = Classificationstore\GroupConfig::getByName($name, $storeId);
 
         if (!$config) {
             $config = new Classificationstore\GroupConfig();
@@ -152,11 +153,11 @@ class ClassificationstoreController extends AdminController implements KernelCon
      *
      * @throws \Exception
      */
     public function createStoreAction(Request $request): JsonResponse
     {
-        $name = $request->get('name');
+        $name = SecurityHelper::getStringWithoutControlChars($request->get('name'));
 
         $config = Classificationstore\StoreConfig::getByName($name);
 
         if (!$config) {
             $config = new Classificationstore\StoreConfig();
@@ -178,11 +179,11 @@ class ClassificationstoreController extends AdminController implements KernelCon
      *
      * @throws \Exception
      */
     public function createCollectionAction(Request $request): JsonResponse
     {
-        $name = $request->get('name');
+        $name = SecurityHelper::getStringWithoutControlChars($request->get('name'));
         $storeId = $request->get('storeId');
         $config = Classificationstore\CollectionConfig::getByName($name, $storeId);
 
         if (!$config) {
             $config = new Classificationstore\CollectionConfig();
diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/helpers.js b/bundles/AdminBundle/Resources/public/js/pimcore/helpers.js
index 3c38c819a6fd..63778fd39d91 100644
--- a/bundles/AdminBundle/Resources/public/js/pimcore/helpers.js
+++ b/bundles/AdminBundle/Resources/public/js/pimcore/helpers.js
@@ -3276,5 +3276,9 @@ pimcore.helpers.treeDragDropValidate = function (node, oldParent, newParent) {
         return false;
     }
 
     return true;
 };
+
+pimcore.helpers.getStringWithoutControlChars = function (text) {
+    return text.replace(/[<>"'`!?/\\%$(){};,:|=]/gi, '');
+};
diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/collectionsPanel.js b/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/collectionsPanel.js
index 8960026226f8..77a994569581 100644
--- a/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/collectionsPanel.js
+++ b/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/collectionsPanel.js
@@ -139,11 +139,11 @@ pimcore.object.classificationstore.collectionsPanel = Class.create({
                     handler: function (grid, rowIndex) {
                         var data = grid.getStore().getAt(rowIndex);
                         var colId = data.data.colId;
                         var groupId = data.data.groupId;
 
-                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection_relation'), data.data.groupName), function(btn) {
+                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection_relation'), pimcore.helpers.getStringWithoutControlChars(data.data.groupName)), function(btn) {
                             if (btn == 'yes') {
                                 Ext.Ajax.request({
                                     url: Routing.generate('pimcore_admin_dataobject_classificationstore_deletecollectionrelation'),
                                     method: 'DELETE',
                                     params: {
@@ -313,11 +313,11 @@ pimcore.object.classificationstore.collectionsPanel = Class.create({
 
                         this.relationsStore.removeAll(true);
                         this.relationsGrid.hide();
                         this.relationsPanel.disable();
 
-                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection'), data.data.name), function(btn) {
+                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection'), pimcore.helpers.getStringWithoutControlChars(data.data.name)), function(btn) {
                             if (btn == 'yes') {
                                 Ext.Ajax.request({
                                     url: Routing.generate('pimcore_admin_dataobject_classificationstore_deletecollection'),
                                     method: 'DELETE',
                                     params: {
@@ -408,12 +408,11 @@ pimcore.object.classificationstore.collectionsPanel = Class.create({
         Ext.MessageBox.prompt(t('classificationstore_mbx_entercollection_title'), t('classificationstore_mbx_entergroup_prompt'),
             this.addFieldComplete.bind(this), null, null, "");
     },
 
     addFieldComplete: function (button, value, object) {
-
-        value = value.trim();
+        value = pimcore.helpers.getStringWithoutControlChars(value).trim();
         if (button == "ok" && value.length > 1) {
             Ext.Ajax.request({
                 url: Routing.generate('pimcore_admin_dataobject_classificationstore_createcollection'),
                 method: 'POST',
                 params: {
diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/groupsPanel.js b/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/groupsPanel.js
index d04b3df64720..6650b5ed8e50 100644
--- a/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/groupsPanel.js
+++ b/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/groupsPanel.js
@@ -146,11 +146,11 @@ pimcore.object.classificationstore.groupsPanel = Class.create({
                     handler: function (grid, rowIndex) {
                         var data = grid.getStore().getAt(rowIndex);
                         var keyId = data.data.keyId;
                         var groupId = data.data.groupId;
 
-                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_relation'), data.data.keyName), function(btn) {
+                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_relation'), pimcore.helpers.getStringWithoutControlChars(data.data.keyName)), function(btn) {
                             if (btn == 'yes') {
                                 Ext.Ajax.request({
                                     url: Routing.generate('pimcore_admin_dataobject_classificationstore_deleterelation'),
                                     method: 'DELETE',
                                     params: {
@@ -317,11 +317,11 @@ pimcore.object.classificationstore.groupsPanel = Class.create({
                     icon: "/bundles/pimcoreadmin/img/flat-color-icons/delete.svg",
                     handler: function (grid, rowIndex) {
                         var data = grid.getStore().getAt(rowIndex);
                         var id = data.data.id;
 
-                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_group'), data.data.name), function(btn) {
+                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_group'), pimcore.helpers.getStringWithoutControlChars(data.data.name)), function(btn) {
                             if (btn == 'yes') {
 
                                 //necessary for aborting all pending proxy requests
                                 //https://github.com/pimcore/pimcore/issues/11284
                                 this.relationsStore.getProxy().destroy();
@@ -417,12 +417,11 @@ pimcore.object.classificationstore.groupsPanel = Class.create({
         Ext.MessageBox.prompt(t('classificationstore_mbx_entergroup_title'), t('classificationstore_mbx_entergroup_prompt'),
             this.addFieldComplete.bind(this), null, null, "");
     },
 
     addFieldComplete: function (button, value, object) {
-
-        value = value.trim();
+        value = pimcore.helpers.getStringWithoutControlChars(value).trim();
         if (button == "ok" && value.length > 1) {
             Ext.Ajax.request({
                 url: Routing.generate('pimcore_admin_dataobject_classificationstore_creategroup'),
                 method: 'POST',
                 params: {
diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/propertiesPanel.js b/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/propertiesPanel.js
index 40a36a1b3b57..7e6386b569d4 100644
--- a/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/propertiesPanel.js
+++ b/bundles/AdminBundle/Resources/public/js/pimcore/object/classificationstore/propertiesPanel.js
@@ -196,11 +196,11 @@ pimcore.object.classificationstore.propertiespanel = Class.create({
                     icon: "/bundles/pimcoreadmin/img/flat-color-icons/delete.svg",
                     handler: function (grid, rowIndex) {
                         var data = grid.getStore().getAt(rowIndex);
                         var id = data.data.id;
 
-                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_property'), data.data.name), function(btn) {
+                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_property'), pimcore.helpers.getStringWithoutControlChars(data.data.name)), function(btn) {
                             if (btn == 'yes') {
                                 Ext.Ajax.request({
                                     url: Routing.generate('pimcore_admin_dataobject_classificationstore_deleteproperty'),
                                     method: 'DELETE',
                                     params: {
@@ -325,12 +325,11 @@ pimcore.object.classificationstore.propertiespanel = Class.create({
         Ext.MessageBox.prompt(t('classificationstore_mbx_enterkey_title'), t('classificationstore_mbx_enterkey_prompt'),
             this.addFieldComplete.bind(this), null, null, "");
     },
 
     addFieldComplete: function (button, value, object) {
-
-        value = value.trim();
+        value = pimcore.helpers.getStringWithoutControlChars(value).trim();
         if (button == "ok" && value.length > 1) {
             Ext.Ajax.request({
                 url: Routing.generate('pimcore_admin_dataobject_classificationstore_addproperty'),
                 method: 'POST',
                 params: {
diff --git a/lib/Security/SecurityHelper.php b/lib/Security/SecurityHelper.php
new file mode 100644
index 000000000000..0eda747507ba
--- /dev/null
+++ b/lib/Security/SecurityHelper.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * Pimcore
+ *
+ * This source file is available under two different licenses:
+ * - GNU General Public License version 3 (GPLv3)
+ * - Pimcore Commercial License (PCL)
+ * Full copyright and license information is available in
+ * LICENSE.md which is distributed with this source code.
+ *
+ *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
+ *  @license    http://www.pimcore.org/license     GPLv3 and PCL
+ */
+
+namespace Pimcore\Security;
+
+class SecurityHelper
+{
+    public static function getStringWithoutControlChars(string $text): string
+    {
+        return preg_replace('[\\\\<>"\'`!?/%$(){};,:|=]','', $text);
+    }
+}

Vulnerable code:         $name = $request->get('name');
        $name = $request->get('name');
        $name = $request->get('name');
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection_relation'), data.data.groupName), function(btn) {
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection'), data.data.name), function(btn) {

        value = value.trim();
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_relation'), data.data.keyName), function(btn) {
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_group'), data.data.name), function(btn) {

        value = value.trim();
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_property'), data.data.name), function(btn) {

        value = value.trim();
Fixed code: use Pimcore\Security\SecurityHelper;
        $name = SecurityHelper::getStringWithoutControlChars($request->get('name'));
        $name = SecurityHelper::getStringWithoutControlChars($request->get('name'));
        $name = SecurityHelper::getStringWithoutControlChars($request->get('name'));

pimcore.helpers.getStringWithoutControlChars = function (text) {
    return text.replace(/[<>"'`!?/\\%$(){};,:|=]/gi, '');
};
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection_relation'), pimcore.helpers.getStringWithoutControlChars(data.data.groupName)), function(btn) {
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_collection'), pimcore.helpers.getStringWithoutControlChars(data.data.name)), function(btn) {
        value = pimcore.helpers.getStringWithoutControlChars(value).trim();
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_relation'), pimcore.helpers.getStringWithoutControlChars(data.data.keyName)), function(btn) {
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_group'), pimcore.helpers.getStringWithoutControlChars(data.data.name)), function(btn) {
        value = pimcore.helpers.getStringWithoutControlChars(value).trim();
                        Ext.Msg.confirm(t('delete'), sprintf(t('delete_message_advanced'), t('classificationstore_property'), pimcore.helpers.getStringWithoutControlChars(data.data.name)), function(btn) {
        value = pimcore.helpers.getStringWithoutControlChars(value).trim();
<?php

/**
 * Pimcore
 *
 * This source file is available under two different licenses:
 * - GNU General Public License version 3 (GPLv3)
 * - Pimcore Commercial License (PCL)
 * Full copyright and license information is available in
 * LICENSE.md which is distributed with this source code.
 *
 *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
 *  @license    http://www.pimcore.org/license     GPLv3 and PCL
 */

namespace Pimcore\Security;

class SecurityHelper
{
    public static function getStringWithoutControlChars(string $text): string
    {
        return preg_replace('[\\\\<>"\'`!?/%$(){};,:|=]','', $text);
    }
}