CVE-2022-24833: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: PrivateBin is minimalist, open source online pastebin clone where the server has zero knowledge of pasted data. In PrivateBin < v1.4.0 a cross-site scripting (XSS) vulnerability was found. The vulnerability is present in all versions from v0.21 of the project, which was at the time still called ZeroBin. The issue is caused by the fact that SVGs can contain JavaScript. This can allow an attacker to execute code, if the user opens a paste with a specifically crafted SVG attachment, and interacts with the preview image and the instance isn't protected by an appropriate content security policy. Users are advised to either upgrade to version 1.4.0 or to ensure the content security policy of their instance is set correctly.
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: 2022-04-11T21:15Z
Repository: https://github.com/PrivateBin/PrivateBin
Language: PHP
Files changed: CHANGELOG.md, js/privatebin.js, tpl/bootstrap.php, tpl/page.php
Commit date: 2022-03-13 19:56:12 +0100
Commit message: Sanitize SVG preview, preventing script execution in instance context, while dropping support for attachment download in IE

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef114663..1fb3ae22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,15 +8,17 @@
     * ADDED: Configuration option to exempt IPs from the rate-limiter (#787)
     * ADDED: Google Cloud Storage backend support (#795)
     * ADDED: Oracle database support (#868)
     * ADDED: Configuration option to limit paste creation and commenting to certain IPs (#883)
     * ADDED: Set CSP also as meta tag, to deal with misconfigured webservers mangling the HTTP header
+    * ADDED: Sanitize SVG preview, preventing script execution in instance context
     * CHANGED: Language selection cookie only transmitted over HTTPS (#472)
     * CHANGED: Upgrading libraries to: base-x 4.0.0, bootstrap 3.4.1 (JS), DOMpurify 2.3.6, ip-lib 1.18.0, jQuery 3.6.0, random_compat 2.0.21 & Showdown 2.0.0
     * CHANGED: Removed automatic `.ini` configuration file migration (#808)
     * CHANGED: Removed configurable `dir` for `traffic` & `purge` limiters (#419)
     * CHANGED: Server salt, traffic and purge limiter now stored in the storage backend (#419)
+    * CHANGED: Drop support for attachment download in IE
   * **1.3.5 (2021-04-05)**
     * ADDED: Translations for Hebrew, Lithuanian, Indonesian and Catalan
     * ADDED: Make the project info configurable (#681)
     * CHANGED: Upgrading libraries to: DOMpurify 2.2.7, kjua 0.9.0 & random_compat 2.0.18
     * CHANGED: Open all links in new window (#630)
diff --git a/js/privatebin.js b/js/privatebin.js
index b6a3226e..53474bd1 100644
--- a/js/privatebin.js
+++ b/js/privatebin.js
@@ -50,10 +50,35 @@ jQuery.PrivateBin = (function($, RawDeflate) {
      *
      * @private
      */
     let z;
 
+    /**
+     * DOMpurify settings for HTML content
+     *
+     * @private
+     */
+     const purifyHtmlConfig = {
+        ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
+        SAFE_FOR_JQUERY: true,
+        USE_PROFILES: {
+            html: true
+        }
+    };
+
+    /**
+     * DOMpurify settings for SVG content
+     *
+     * @private
+     */
+     const purifySvgConfig = {
+        USE_PROFILES: {
+            svg: true,
+            svgFilters: true
+        }
+    };
+
     /**
      * CryptoData class
      *
      * bundles helper fuctions used in both paste and comment formats
      *
@@ -407,11 +432,12 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             element.html(
                 DOMPurify.sanitize(
                     element.html().replace(
                         /(((https?|ftp):\/\/[\w?!=&.\/-;#@~%+*-]+(?![\w\s?!&.\/;#~%"=-]>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig,
                         '<a href="$1" rel="nofollow noopener noreferrer">$1</a>'
-                    )
+                    ),
+                    purifyHtmlConfig
                 )
             );
         };
 
         /**
@@ -2534,11 +2560,12 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                     excludeTrailingPunctuationFromURLs: true
                 });
                 // let showdown convert the HTML and sanitize HTML *afterwards*!
                 $plainText.html(
                     DOMPurify.sanitize(
-                        converter.makeHtml(text)
+                        converter.makeHtml(text),
+                        purifyHtmlConfig
                     )
                 );
                 // add table classes from bootstrap css
                 $plainText.find('table').addClass('table-condensed table-bordered');
             } else {
@@ -2750,57 +2777,80 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             $dragAndDropFileName,
             attachmentHasPreview = false,
             $dropzone;
 
         /**
+         * get blob URL from string data and mime type
+         *
+         * @name   AttachmentViewer.getBlobUrl
+         * @private
+         * @function
+         * @param {string} data - raw data of attachment
+         * @param {string} data - mime type of attachment
+         * @return {string} objectURL
+         */
+         function getBlobUrl(data, mimeType)
+         {
+            // Transform into a Blob
+            const buf = new Uint8Array(data.length);
+            for (let i = 0; i < data.length; ++i) {
+                buf[i] = data.charCodeAt(i);
+            }
+            const blob = new window.Blob(
+                [buf],
+                {
+                    type: mimeType
+                }
+            );
+
+            // Get Blob URL
+            return window.URL.createObjectURL(blob);
+         }
+
+         /**
          * sets the attachment but does not yet show it
          *
          * @name   AttachmentViewer.setAttachment
          * @function
          * @param {string} attachmentData - base64-encoded data of file
          * @param {string} fileName - optional, file name
          */
         me.setAttachment = function(attachmentData, fileName)
         {
-            // data URI format: data:[<mediaType>][;base64],<data>
+            // data URI format: data:[<mimeType>][;base64],<data>
 
             // position in data URI string of where data begins
             const base64Start = attachmentData.indexOf(',') + 1;
-            // position in data URI string of where mediaType ends
-            const mediaTypeEnd = attachmentData.indexOf(';');
+            // position in data URI string of where mimeType ends
+            const mimeTypeEnd = attachmentData.indexOf(';');
 
-            // extract mediaType
-            const mediaType = attachmentData.substring(5, mediaTypeEnd);
+            // extract mimeType
+            const mimeType = attachmentData.substring(5, mimeTypeEnd);
             // extract data and convert to binary
             const rawData = attachmentData.substring(base64Start);
             const decodedData = rawData.length > 0 ? atob(rawData) : '';
 
-            // Transform into a Blob
-            const buf = new Uint8Array(decodedData.length);
-            for (let i = 0; i < decodedData.length; ++i) {
-                buf[i] = decodedData.charCodeAt(i);
-            }
-            const blob = new window.Blob([ buf ], { type: mediaType });
-
-            // Get Blob URL
-            const blobUrl = window.URL.createObjectURL(blob);
-
-            // IE does not support setting a data URI on an a element
-            // Using msSaveBlob to download
-            if (window.Blob && navigator.msSaveBlob) {
-                $attachmentLink.off('click').on('click', function () {
-                    navigator.msSaveBlob(blob, fileName);
-                });
-            } else {
-                $attachmentLink.attr('href', blobUrl);
-            }
+            let blobUrl = getBlobUrl(decodedData, mimeType);
+            $attachmentLink.attr('href', blobUrl);
 
             if (typeof fileName !== 'undefined') {
                 $attachmentLink.attr('download', fileName);
             }
 
-            me.handleBlobAttachmentPreview($attachmentPreview, blobUrl, mediaType);
+            // sanitize SVG preview
+            // prevents executing embedded scripts when CSP is not set and user
+            // right-clicks/long-taps and opens the SVG in a new tab - prevented
+            // in the preview by use of an img tag, which disables scripts, too
+            if (mimeType.match(/image\/svg/i)) {
+                const sanitizedData = DOMPurify.sanitize(
+                    decodedData,
+                    purifySvgConfig
+                );
+                blobUrl = getBlobUrl(sanitizedData, mimeType);
+            }
+
+            me.handleBlobAttachmentPreview($attachmentPreview, blobUrl, mimeType);
         };
 
         /**
          * displays the attachment
          *
@@ -3663,11 +3713,18 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                   newDoc = document.open('text/html', 'replace');
             newDoc.write('<!DOCTYPE html><html><head>');
             for (let i = 0; i < $head.length; ++i) {
                 newDoc.write($head[i].outerHTML);
             }
-            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(Helper.htmlEntities(paste)) + '</pre></body></html>');
+            newDoc.write(
+                '</head><body><pre>' +
+                DOMPurify.sanitize(
+                    Helper.htmlEntities(paste),
+                    purifyHtmlConfig
+                ) +
+                '</pre></body></html>'
+            );
             newDoc.close();
         }
 
         /**
          * download text
@@ -5392,15 +5449,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
         me.init = function()
         {
             // first load translations
             I18n.loadTranslations();
 
-            DOMPurify.setConfig({
-                ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
-                SAFE_FOR_JQUERY: true
-            });
-
             // Add a hook to make all links open a new window
             DOMPurify.addHook('afterSanitizeAttributes', function(node) {
                 // set all elements owning target to target=_blank
                 if ('target' in node && node.id !== 'pasteurl') {
                     node.setAttribute('target', '_blank');
diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php
index 1fc8feb2..4c688784 100644
--- a/tpl/bootstrap.php
+++ b/tpl/bootstrap.php
@@ -71,11 +71,11 @@ if ($MARKDOWN) :
 <?php
 endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.3.6.js" integrity="sha512-N1GGPjbqLbwK821ZN7C925WuTwU4aDxz2CEEOXQ6/s6m6MBwVj8fh5fugiE2hzsm0xud3q7jpjZQ4ILnpMREYQ==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-EdUms2nI12Cmtv014stIEBlyPjeKMHlkg7NiBJup1b7jJF5amKhev2RwTaldINXK4UaWbZtQ6hGuMPNvvNQZFA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-5fNML95dptjIMzFG4KAGE2nNbORbyrCWq4ql9tkvoUF2HSnemlxzngQ5PlWhpi0J2ttKIEqfGMBjU5MywzzdWQ==" crossorigin="anonymous"></script>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png" sizes="16x16" />
 		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
diff --git a/tpl/page.php b/tpl/page.php
index 74254c8e..ca84e8f6 100644
--- a/tpl/page.php
+++ b/tpl/page.php
@@ -49,11 +49,11 @@ if ($MARKDOWN):
 <?php
 endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.3.6.js" integrity="sha512-N1GGPjbqLbwK821ZN7C925WuTwU4aDxz2CEEOXQ6/s6m6MBwVj8fh5fugiE2hzsm0xud3q7jpjZQ4ILnpMREYQ==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-EdUms2nI12Cmtv014stIEBlyPjeKMHlkg7NiBJup1b7jJF5amKhev2RwTaldINXK4UaWbZtQ6hGuMPNvvNQZFA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-5fNML95dptjIMzFG4KAGE2nNbORbyrCWq4ql9tkvoUF2HSnemlxzngQ5PlWhpi0J2ttKIEqfGMBjU5MywzzdWQ==" crossorigin="anonymous"></script>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
 		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />

Vulnerable code:                     )
                        converter.makeHtml(text)
            // data URI format: data:[<mediaType>][;base64],<data>
            // position in data URI string of where mediaType ends
            const mediaTypeEnd = attachmentData.indexOf(';');
            // extract mediaType
            const mediaType = attachmentData.substring(5, mediaTypeEnd);
            // Transform into a Blob
            const buf = new Uint8Array(decodedData.length);
            for (let i = 0; i < decodedData.length; ++i) {
                buf[i] = decodedData.charCodeAt(i);
            }
            const blob = new window.Blob([ buf ], { type: mediaType });

            // Get Blob URL
            const blobUrl = window.URL.createObjectURL(blob);

            // IE does not support setting a data URI on an a element
            // Using msSaveBlob to download
            if (window.Blob && navigator.msSaveBlob) {
                $attachmentLink.off('click').on('click', function () {
                    navigator.msSaveBlob(blob, fileName);
                });
            } else {
                $attachmentLink.attr('href', blobUrl);
            }
            me.handleBlobAttachmentPreview($attachmentPreview, blobUrl, mediaType);
            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(Helper.htmlEntities(paste)) + '</pre></body></html>');
            DOMPurify.setConfig({
                ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
                SAFE_FOR_JQUERY: true
            });

		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-EdUms2nI12Cmtv014stIEBlyPjeKMHlkg7NiBJup1b7jJF5amKhev2RwTaldINXK4UaWbZtQ6hGuMPNvvNQZFA==" crossorigin="anonymous"></script>
		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-EdUms2nI12Cmtv014stIEBlyPjeKMHlkg7NiBJup1b7jJF5amKhev2RwTaldINXK4UaWbZtQ6hGuMPNvvNQZFA==" crossorigin="anonymous"></script>
Fixed code:     * ADDED: Sanitize SVG preview, preventing script execution in instance context
    * CHANGED: Drop support for attachment download in IE
    /**
     * DOMpurify settings for HTML content
     *
     * @private
     */
     const purifyHtmlConfig = {
        ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
        SAFE_FOR_JQUERY: true,
        USE_PROFILES: {
            html: true
        }
    };

    /**
     * DOMpurify settings for SVG content
     *
     * @private
     */
     const purifySvgConfig = {
        USE_PROFILES: {
            svg: true,
            svgFilters: true
        }
    };

                    ),
                    purifyHtmlConfig
                        converter.makeHtml(text),
                        purifyHtmlConfig
         * get blob URL from string data and mime type
         *
         * @name   AttachmentViewer.getBlobUrl
         * @private
         * @function
         * @param {string} data - raw data of attachment
         * @param {string} data - mime type of attachment
         * @return {string} objectURL
         */
         function getBlobUrl(data, mimeType)
         {
            // Transform into a Blob
            const buf = new Uint8Array(data.length);
            for (let i = 0; i < data.length; ++i) {
                buf[i] = data.charCodeAt(i);
            }
            const blob = new window.Blob(
                [buf],
                {
                    type: mimeType
                }
            );

            // Get Blob URL
            return window.URL.createObjectURL(blob);
         }

         /**
            // data URI format: data:[<mimeType>][;base64],<data>
            // position in data URI string of where mimeType ends
            const mimeTypeEnd = attachmentData.indexOf(';');
            // extract mimeType
            const mimeType = attachmentData.substring(5, mimeTypeEnd);
            let blobUrl = getBlobUrl(decodedData, mimeType);
            $attachmentLink.attr('href', blobUrl);
            // sanitize SVG preview
            // prevents executing embedded scripts when CSP is not set and user
            // right-clicks/long-taps and opens the SVG in a new tab - prevented
            // in the preview by use of an img tag, which disables scripts, too
            if (mimeType.match(/image\/svg/i)) {
                const sanitizedData = DOMPurify.sanitize(
                    decodedData,
                    purifySvgConfig
                );
                blobUrl = getBlobUrl(sanitizedData, mimeType);
            }

            me.handleBlobAttachmentPreview($attachmentPreview, blobUrl, mimeType);
            newDoc.write(
                '</head><body><pre>' +
                DOMPurify.sanitize(
                    Helper.htmlEntities(paste),
                    purifyHtmlConfig
                ) +
                '</pre></body></html>'
            );
		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-5fNML95dptjIMzFG4KAGE2nNbORbyrCWq4ql9tkvoUF2HSnemlxzngQ5PlWhpi0J2ttKIEqfGMBjU5MywzzdWQ==" crossorigin="anonymous"></script>
		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-5fNML95dptjIMzFG4KAGE2nNbORbyrCWq4ql9tkvoUF2HSnemlxzngQ5PlWhpi0J2ttKIEqfGMBjU5MywzzdWQ==" crossorigin="anonymous"></script>