CVE-2023-30855: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Pimcore is an open source data and experience management platform. Versions of Pimcore prior to 10.5.18 are vulnerable to path traversal. The impact of this path traversal and arbitrary extension is limited to creation of arbitrary files and appending data to existing files. When combined with the SQL Injection, the exported data `RESTRICTED DIFFUSION 9 / 9` can be controlled and a webshell can be uploaded. Attackers can use that to execute arbitrary PHP code on the server with the permissions of the webserver. Users may upgrade to version 10.5.18 to receive a patch or, as a workaround, apply the patch manually.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.5
Published: 2023-05-08T18: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);
    }
}