CVE-2023-46722: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: The Pimcore Admin Classic Bundle provides a backend UI for Pimcore. Prior to version 1.2.0, a cross-site scripting vulnerability has the potential to steal a user's cookie and gain unauthorized access to that user's account through the stolen cookie or redirect users to other malicious sites. Users should upgrade to version 1.2.0 to receive a patch or, as a workaround, apply the patch manually.
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.1
Published: 2023-10-31T16:15Z
Repository: https://github.com/pimcore/admin-ui-classic-bundle
Language: Other
Files changed: src/Controller/Admin/Asset/AssetController.php, templates/admin/asset/get_preview_pdf_in_progress.html.twig, templates/admin/asset/get_preview_pdf_unsafe.html.twig, templates/admin/asset/show_version_document.html.twig, translations/admin.en.yaml
Commit date: 2023-10-09 11:00:29 +0200
Commit message: [Improvement]: Add sanitizing pdf (#301)

* Improve: add sanitizing pdf

* Review changes - use scan instead of sanitizing

* Review changes

* Update translations/admin.en.yaml

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Review changes

* Update src/Controller/Admin/Asset/AssetController.php

Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>

* Update src/Controller/Admin/Asset/AssetController.php

Co-authored-by: aryaantony92 <97134765+aryaantony92@users.noreply.github.com>

---------

Co-authored-by: Divesh Pahuja <divesh.pahuja@pimcore.com>
Co-authored-by: Jacob Dreesen <jacob@hdreesen.de>
Co-authored-by: aryaantony92 <97134765+aryaantony92@users.noreply.github.com>
Diff: diff --git a/src/Controller/Admin/Asset/AssetController.php b/src/Controller/Admin/Asset/AssetController.php
index 9d11ad962b..37e34e9d4f 100644
--- a/src/Controller/Admin/Asset/AssetController.php
+++ b/src/Controller/Admin/Asset/AssetController.php
@@ -31,10 +31,11 @@ use Pimcore\Event\AssetEvents;
 use Pimcore\Event\Model\Asset\ResolveUploadTargetEvent;
 use Pimcore\File;
 use Pimcore\Loader\ImplementationLoader\Exception\UnsupportedException;
 use Pimcore\Logger;
 use Pimcore\Messenger\AssetPreviewImageMessage;
+use Pimcore\Messenger\AssetUpdateTasksMessage;
 use Pimcore\Model;
 use Pimcore\Model\Asset;
 use Pimcore\Model\DataObject\ClassDefinition\Data\ManyToManyRelation;
 use Pimcore\Model\DataObject\Concrete;
 use Pimcore\Model\Element;
@@ -982,10 +983,17 @@ class AssetController extends ElementControllerBase implements KernelControllerE
 
         if (!$asset->isAllowed('versions')) {
             throw $this->createAccessDeniedHttpException('Permission denied, version id [' . $id . ']');
         }
 
+        if ($asset->getMimeType() === 'application/pdf') {
+            $scanResponse = $this->getResponseByScanStatus($asset, false);
+            if ($scanResponse) {
+                return $scanResponse;
+            }
+        }
+
         $loader = \Pimcore::getContainer()->get('pimcore.implementation_loader.asset.metadata.data');
 
         return $this->render(
             '@PimcoreAdmin/admin/asset/show_version_' . strtolower($asset->getType()) . '.html.twig',
             [
@@ -1485,24 +1493,30 @@ class AssetController extends ElementControllerBase implements KernelControllerE
         $response->headers->set('Pragma', '');
     }
 
     /**
      * @Route("/get-preview-document", name="pimcore_admin_asset_getpreviewdocument", methods={"GET"})
-     *
-     * @param Request $request
-     *
-     * @return StreamedResponse
      */
-    public function getPreviewDocumentAction(Request $request): StreamedResponse
+    public function getPreviewDocumentAction(
+        Request $request,
+        TranslatorInterface $translator
+    ): StreamedResponse|Response
     {
         $asset = Asset\Document::getById((int) $request->get('id'));
 
         if (!$asset) {
             throw $this->createNotFoundException('could not load document asset');
         }
 
         if ($asset->isAllowed('view')) {
+            if ($asset->getMimeType() === 'application/pdf') {
+                $scanResponse = $this->getResponseByScanStatus($asset);
+                if ($scanResponse) {
+                    return $scanResponse;
+                }
+            }
+
             $stream = $this->getDocumentPreviewPdf($asset);
             if ($stream) {
                 return new StreamedResponse(function () use ($stream) {
                     fpassthru($stream);
                 }, 200, [
@@ -1514,10 +1528,33 @@ class AssetController extends ElementControllerBase implements KernelControllerE
         } else {
             throw $this->createAccessDeniedException('Access to asset ' . $asset->getId() . ' denied');
         }
     }
 
+    private function getResponseByScanStatus(Asset\Document $asset, bool $processBackground = true) :?Response
+    {
+        if (!Config::getSystemConfiguration('assets')['document']['scan_pdf']) {
+            return null;
+        }
+
+        $scanStatus = $asset->getScanStatus();
+        if ($scanStatus === null) {
+            $scanStatus = Asset\Enum\PdfScanStatus::IN_PROGRESS;
+            if ($processBackground) {
+                \Pimcore::getContainer()->get('messenger.bus.pimcore-core')->dispatch(
+                    new AssetUpdateTasksMessage($asset->getId())
+                );
+            }
+        }
+
+        return match($scanStatus) {
+            Asset\Enum\PdfScanStatus::IN_PROGRESS => $this->render('@PimcoreAdmin/admin/asset/get_preview_pdf_in_progress.html.twig'),
+            Asset\Enum\PdfScanStatus::UNSAFE => $this->render('@PimcoreAdmin/admin/asset/get_preview_pdf_unsafe.html.twig'),
+            default => null,
+        };
+    }
+
     /**
      * @param Asset\Document $asset
      *
      * @return resource|null
      */
@@ -1527,11 +1564,16 @@ class AssetController extends ElementControllerBase implements KernelControllerE
 
         if ($asset->getMimeType() == 'application/pdf') {
             $stream = $asset->getStream();
         }
 
-        if (!$stream && $asset->getPageCount() && \Pimcore\Document::isAvailable() && \Pimcore\Document::isFileTypeSupported($asset->getFilename())) {
+        if (
+            !$stream &&
+            $asset->getPageCount() &&
+            \Pimcore\Document::isAvailable() &&
+            \Pimcore\Document::isFileTypeSupported($asset->getFilename())
+        ) {
             try {
                 $document = \Pimcore\Document::getInstance();
                 $stream = $document->getPdf($asset);
             } catch (\Exception $e) {
                 // nothing to do
diff --git a/templates/admin/asset/get_preview_pdf_in_progress.html.twig b/templates/admin/asset/get_preview_pdf_in_progress.html.twig
new file mode 100644
index 0000000000..391ed010c3
--- /dev/null
+++ b/templates/admin/asset/get_preview_pdf_in_progress.html.twig
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+
+    <style>
+
+        /* hide from ie on mac \*/
+        html {
+            height: 100%;
+            overflow: hidden;
+        }
+        /* end hide */
+
+        body {
+            height: 100%;
+            margin: 0;
+            padding: 0;
+            background: #EEE;
+        }
+
+        #container {
+            text-align: center;
+            position: absolute;
+            top:50%;
+            margin-top: -200px;
+            width: 100%;
+        }
+
+        #message {
+            margin-left: 8px;
+        }
+
+    </style>
+
+    <script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
+        window.setTimeout(() => {
+            window.location.reload();
+        }, 5000)
+    </script>
+</head>
+
+<body>
+
+<div id="container">
+    <span id="message">{{ 'pdf_scan_in_progress'|trans([], 'admin') }}</span>
+</div>
+
+
+</body>
+</html>
diff --git a/templates/admin/asset/get_preview_pdf_unsafe.html.twig b/templates/admin/asset/get_preview_pdf_unsafe.html.twig
new file mode 100644
index 0000000000..b28db8a7fc
--- /dev/null
+++ b/templates/admin/asset/get_preview_pdf_unsafe.html.twig
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+
+    <style>
+
+        /* hide from ie on mac \*/
+        html {
+            height: 100%;
+            overflow: hidden;
+        }
+        /* end hide */
+
+        body {
+            height: 100%;
+            margin: 0;
+            padding: 0;
+            background: #EEE;
+        }
+
+        #container {
+            text-align: center;
+            position: absolute;
+            top:50%;
+            margin-top: -200px;
+            width: 100%;
+        }
+
+        #warning {
+            position: relative;
+            width: 40px;
+            height: 40px;
+            top: 13px;
+        }
+
+        #message {
+            margin-left: 8px;
+        }
+
+    </style>
+
+</head>
+
+<body>
+
+<div id="container">
+    <img alt="warning" id="warning" src="/bundles/pimcoreadmin/img/flat-color-icons/overlay-error.svg" />
+    <span id="message">{{ 'pdf_js_unsafe'|trans([], 'admin') }}</span>
+</div>
+
+
+</body>
+</html>
diff --git a/templates/admin/asset/show_version_document.html.twig b/templates/admin/asset/show_version_document.html.twig
index f7291159c9..a794664b58 100644
--- a/templates/admin/asset/show_version_document.html.twig
+++ b/templates/admin/asset/show_version_document.html.twig
@@ -1,9 +1,9 @@
 {% if asset.getMimeType() == 'application/pdf' %}
     {% set tempFile = asset.getTemporaryFile() %}
     {% set dataUri = pimcore_asset_version_preview(tempFile) %}
-    
+
     <div style="display: flex; width: 100%; height: 100%; flex-direction: column; overflow: hidden;">
         <iframe src="{{ dataUri }}" frameborder="0" style="flex-grow: 1; border: none; margin: 0; padding: 0;"></iframe>
     </div>
 {% else %}
     {% include '@PimcoreAdmin/admin/asset/show_version_unknown.html.twig' %}
diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml
index f144fdcc15..f7e10b6db3 100644
--- a/translations/admin.en.yaml
+++ b/translations/admin.en.yaml
@@ -986,5 +986,7 @@ delete_entire_folder_question: This operation will delete the entire parent/fold
 allow_to_clear_relation: Allow to clear all relations of this field
 allow_asset_inline_download: Allow inline download
 system_appearance_settings: 'Appearance & Branding'
 male: Male
 female: Female
+pdf_js_unsafe: This PDF file contains JavaScript. If you want to view it, please download and open it in your local PDF viewer.
+pdf_scan_in_progress: 'Preview not available: PDF is being scanned. This may take a while.'

Vulnerable code:      *
     * @param Request $request
     *
     * @return StreamedResponse
    public function getPreviewDocumentAction(Request $request): StreamedResponse
        if (!$stream && $asset->getPageCount() && \Pimcore\Document::isAvailable() && \Pimcore\Document::isFileTypeSupported($asset->getFilename())) {
    
Fixed code: use Pimcore\Messenger\AssetUpdateTasksMessage;
        if ($asset->getMimeType() === 'application/pdf') {
            $scanResponse = $this->getResponseByScanStatus($asset, false);
            if ($scanResponse) {
                return $scanResponse;
            }
        }

    public function getPreviewDocumentAction(
        Request $request,
        TranslatorInterface $translator
    ): StreamedResponse|Response
            if ($asset->getMimeType() === 'application/pdf') {
                $scanResponse = $this->getResponseByScanStatus($asset);
                if ($scanResponse) {
                    return $scanResponse;
                }
            }

    private function getResponseByScanStatus(Asset\Document $asset, bool $processBackground = true) :?Response
    {
        if (!Config::getSystemConfiguration('assets')['document']['scan_pdf']) {
            return null;
        }

        $scanStatus = $asset->getScanStatus();
        if ($scanStatus === null) {
            $scanStatus = Asset\Enum\PdfScanStatus::IN_PROGRESS;
            if ($processBackground) {
                \Pimcore::getContainer()->get('messenger.bus.pimcore-core')->dispatch(
                    new AssetUpdateTasksMessage($asset->getId())
                );
            }
        }

        return match($scanStatus) {
            Asset\Enum\PdfScanStatus::IN_PROGRESS => $this->render('@PimcoreAdmin/admin/asset/get_preview_pdf_in_progress.html.twig'),
            Asset\Enum\PdfScanStatus::UNSAFE => $this->render('@PimcoreAdmin/admin/asset/get_preview_pdf_unsafe.html.twig'),
            default => null,
        };
    }

        if (
            !$stream &&
            $asset->getPageCount() &&
            \Pimcore\Document::isAvailable() &&
            \Pimcore\Document::isFileTypeSupported($asset->getFilename())
        ) {
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">

    <style>

        /* hide from ie on mac \*/
        html {
            height: 100%;
            overflow: hidden;
        }
        /* end hide */

        body {
            height: 100%;
            margin: 0;
            padding: 0;
            background: #EEE;
        }

        #container {
            text-align: center;
            position: absolute;
            top:50%;
            margin-top: -200px;
            width: 100%;
        }

        #message {
            margin-left: 8px;
        }

    </style>

    <script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
        window.setTimeout(() => {
            window.location.reload();
        }, 5000)
    </script>
</head>

<body>

<div id="container">
    <span id="message">{{ 'pdf_scan_in_progress'|trans([], 'admin') }}</span>
</div>


</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">

    <style>

        /* hide from ie on mac \*/
        html {
            height: 100%;
            overflow: hidden;
        }
        /* end hide */

        body {
            height: 100%;
            margin: 0;
            padding: 0;
            background: #EEE;
        }

        #container {
            text-align: center;
            position: absolute;
            top:50%;
            margin-top: -200px;
            width: 100%;
        }

        #warning {
            position: relative;
            width: 40px;
            height: 40px;
            top: 13px;
        }

        #message {
            margin-left: 8px;
        }

    </style>

</head>

<body>

<div id="container">
    <img alt="warning" id="warning" src="/bundles/pimcoreadmin/img/flat-color-icons/overlay-error.svg" />
    <span id="message">{{ 'pdf_js_unsafe'|trans([], 'admin') }}</span>
</div>


</body>
</html>

pdf_js_unsafe: This PDF file contains JavaScript. If you want to view it, please download and open it in your local PDF viewer.
pdf_scan_in_progress: 'Preview not available: PDF is being scanned. This may take a while.'