CVE-2022-0565: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting in Packagist pimcore/pimcore prior to 10.3.1.


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: 6.4
Published: 2022-02-14T12:15Z
Repository: https://github.com/pimcore/pimcore
Language: PHP
Files changed: bundles/AdminBundle/Controller/Admin/SettingsController.php, bundles/CoreBundle/EventListener/AssetSanitizationListener.php, bundles/CoreBundle/Resources/config/event_listeners.yaml, composer.json
Commit date: 2022-02-11 13:29:01 +0100
Commit message: Svg sanitization (#11386)

* setting up the svg saniziter on logo and assets upload

* more detailed exception message

* changed to mime type check instead of file extension

* adding the sanitization to "Upload new version"

* refactor to sanitize on preAdd/preUpdate, rollback AssetController.php

* fix resource|string, null given on mime_content_type

* using symfony mime component + small tweaks

* avoiding save without changes case

* tweak when checking if is image type
Diff: diff --git a/bundles/AdminBundle/Controller/Admin/SettingsController.php b/bundles/AdminBundle/Controller/Admin/SettingsController.php
index d3480f9dee73..e27db6a75ec3 100644
--- a/bundles/AdminBundle/Controller/Admin/SettingsController.php
+++ b/bundles/AdminBundle/Controller/Admin/SettingsController.php
@@ -44,12 +44,15 @@ use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\StreamedResponse;
 use Symfony\Component\HttpKernel\Event\TerminateEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\HttpKernel\KernelInterface;
+use Symfony\Component\Mime\MimeTypes;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Yaml\Yaml;
+use enshrined\svgSanitize\Sanitizer;
+
 
 /**
  * @Route("/settings")
  *
  * @internal
@@ -102,21 +105,34 @@ class SettingsController extends AdminController
      *
      * @throws \Exception
      */
     public function uploadCustomLogoAction(Request $request)
     {
+        $sourcePath = $_FILES['Filedata']['tmp_name'];
         $fileExt = File::getFileExtension($_FILES['Filedata']['name']);
         if (!in_array($fileExt, ['svg', 'png', 'jpg'])) {
             throw new \Exception('Unsupported file format');
         }
 
-        if ($fileExt === 'svg' && stripos(file_get_contents($_FILES['Filedata']['tmp_name']), '<script')) {
-            throw new \Exception('Scripts in SVG files are not supported');
-        }
 
         $storage = Tool\Storage::get('admin');
-        $storage->writeStream(self::CUSTOM_LOGO_PATH, fopen($_FILES['Filedata']['tmp_name'], 'rb'));
+
+        $fileMimeType = MimeTypes::getDefault()->guessMimeType($sourcePath);
+
+        if ($fileMimeType === 'image/svg+xml') {
+            $fileContent = file_get_contents($sourcePath);
+
+            $sanitizer = new Sanitizer();
+            $sanitizedFileContent = $sanitizer->sanitize($fileContent);
+            if ($sanitizedFileContent) {
+                $storage->write(self::CUSTOM_LOGO_PATH, $sanitizedFileContent);
+            }else{
+                throw new \Exception('SVG Sanitization failed, probably due badly formatted XML. Filename:'.$sourcePath);
+            }
+        }else {
+            $storage->writeStream(self::CUSTOM_LOGO_PATH, fopen($sourcePath, 'rb'));
+        }
 
         // set content-type to text/html, otherwise (when application/json is sent) chrome will complain in
         // Ext.form.Action.Submit and mark the submission as failed
 
         $response = $this->adminJson(['success' => true]);
diff --git a/bundles/CoreBundle/EventListener/AssetSanitizationListener.php b/bundles/CoreBundle/EventListener/AssetSanitizationListener.php
new file mode 100644
index 000000000000..7463952ac6b3
--- /dev/null
+++ b/bundles/CoreBundle/EventListener/AssetSanitizationListener.php
@@ -0,0 +1,80 @@
+<?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\Bundle\CoreBundle\EventListener;
+
+
+use Pimcore\Event\AssetEvents;
+use Pimcore\Event\Model\ElementEventInterface;
+use Pimcore\Model\Asset;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use enshrined\svgSanitize\Sanitizer;
+use Symfony\Component\Mime\MimeTypes;
+
+/**
+ * @internal
+ */
+class AssetSanitizationListener implements EventSubscriberInterface
+{
+    public static function getSubscribedEvents()
+    {
+        return [
+            AssetEvents::PRE_ADD => 'sanitizeAsset',
+            AssetEvents::PRE_UPDATE => 'sanitizeAsset',
+        ];
+    }
+
+    /**
+     * @param ElementEventInterface $e
+     */
+    public function sanitizeAsset(ElementEventInterface $e)
+    {
+        $element = $e->getElement();
+
+        if ($element instanceof Asset\Image && $element->getDataChanged()) {
+            $assetStream = $element->getStream();
+
+            if (isset($assetStream)) {
+                $streamMetaData = stream_get_meta_data($assetStream);
+                $mime = MimeTypes::getDefault()->guessMimeType($streamMetaData['uri']);
+
+                if ($mime === 'image/svg+xml') {
+                    $sanitizedData = $this->sanitizeSVG(stream_get_contents($assetStream));
+                    $element->setData($sanitizedData);
+                }
+            }
+        }
+    }
+
+    /**
+     * @param string $fileContent
+     *
+     * @return string
+     *
+     * @throws \Exception
+     */
+
+    protected function sanitizeSVG(string $fileContent)
+    {
+        $sanitizer = new Sanitizer();
+        $sanitizedFileContent = $sanitizer->sanitize($fileContent);
+
+        if (!$sanitizedFileContent) {
+            throw new \Exception('SVG Sanitization failed, probably due badly formatted XML.');
+        }
+
+        return $sanitizedFileContent;
+    }
+}
diff --git a/bundles/CoreBundle/Resources/config/event_listeners.yaml b/bundles/CoreBundle/Resources/config/event_listeners.yaml
index be8b844b5cf1..b0a58aaeebb7 100644
--- a/bundles/CoreBundle/Resources/config/event_listeners.yaml
+++ b/bundles/CoreBundle/Resources/config/event_listeners.yaml
@@ -145,5 +145,7 @@ services:
         arguments:
             $excludeRoutes: '%pimcore.web_profiler.toolbar.excluded_routes%'
             $debugToolbarListener: '@?web_profiler.debug_toolbar'
 
     Pimcore\Bundle\CoreBundle\EventListener\MessengerClearRuntimeCacheListener: ~
+
+    Pimcore\Bundle\CoreBundle\EventListener\AssetSanitizationListener: ~
\ No newline at end of file
diff --git a/composer.json b/composer.json
index c38263cd8b32..f7afbd8a9db2 100644
--- a/composer.json
+++ b/composer.json
@@ -57,10 +57,11 @@
     "doctrine/doctrine-migrations-bundle": "^3.0.2",
     "doctrine/inflector": "^1.4.2 || ^2",
     "symfony/doctrine-messenger": "^5.2.0",
     "egulias/email-validator": "^3.0.0",
     "endroid/qr-code": "^4",
+    "enshrined/svg-sanitize": "^0.14.1",
     "friendsofsymfony/jsrouting-bundle": "^2.7",
     "geoip2/geoip2": "^2.9",
     "google/apiclient": "^2.10",
     "guzzlehttp/guzzle": "^7.2",
     "http-interop/http-factory-guzzle": "^1.0.0",

Vulnerable code:         if ($fileExt === 'svg' && stripos(file_get_contents($_FILES['Filedata']['tmp_name']), '<script')) {
            throw new \Exception('Scripts in SVG files are not supported');
        }
        $storage->writeStream(self::CUSTOM_LOGO_PATH, fopen($_FILES['Filedata']['tmp_name'], 'rb'));
Fixed code: use Symfony\Component\Mime\MimeTypes;
use enshrined\svgSanitize\Sanitizer;

        $sourcePath = $_FILES['Filedata']['tmp_name'];

        $fileMimeType = MimeTypes::getDefault()->guessMimeType($sourcePath);

        if ($fileMimeType === 'image/svg+xml') {
            $fileContent = file_get_contents($sourcePath);

            $sanitizer = new Sanitizer();
            $sanitizedFileContent = $sanitizer->sanitize($fileContent);
            if ($sanitizedFileContent) {
                $storage->write(self::CUSTOM_LOGO_PATH, $sanitizedFileContent);
            }else{
                throw new \Exception('SVG Sanitization failed, probably due badly formatted XML. Filename:'.$sourcePath);
            }
        }else {
            $storage->writeStream(self::CUSTOM_LOGO_PATH, fopen($sourcePath, 'rb'));
        }
<?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\Bundle\CoreBundle\EventListener;


use Pimcore\Event\AssetEvents;
use Pimcore\Event\Model\ElementEventInterface;
use Pimcore\Model\Asset;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use enshrined\svgSanitize\Sanitizer;
use Symfony\Component\Mime\MimeTypes;

/**
 * @internal
 */
class AssetSanitizationListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            AssetEvents::PRE_ADD => 'sanitizeAsset',
            AssetEvents::PRE_UPDATE => 'sanitizeAsset',
        ];
    }

    /**
     * @param ElementEventInterface $e
     */
    public function sanitizeAsset(ElementEventInterface $e)
    {
        $element = $e->getElement();

        if ($element instanceof Asset\Image && $element->getDataChanged()) {
            $assetStream = $element->getStream();

            if (isset($assetStream)) {
                $streamMetaData = stream_get_meta_data($assetStream);
                $mime = MimeTypes::getDefault()->guessMimeType($streamMetaData['uri']);

                if ($mime === 'image/svg+xml') {
                    $sanitizedData = $this->sanitizeSVG(stream_get_contents($assetStream));
                    $element->setData($sanitizedData);
                }
            }
        }
    }

    /**
     * @param string $fileContent
     *
     * @return string
     *
     * @throws \Exception
     */

    protected function sanitizeSVG(string $fileContent)
    {
        $sanitizer = new Sanitizer();
        $sanitizedFileContent = $sanitizer->sanitize($fileContent);

        if (!$sanitizedFileContent) {
            throw new \Exception('SVG Sanitization failed, probably due badly formatted XML.');
        }

        return $sanitizedFileContent;
    }
}

    Pimcore\Bundle\CoreBundle\EventListener\AssetSanitizationListener: ~
    "enshrined/svg-sanitize": "^0.14.1",