From 2a4d572c1e9eb9b608d32b0cc0cb3b6c3b684eab Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 13 Mar 2022 19:56:12 +0100 Subject: [PATCH] Sanitize SVG preview, preventing script execution in instance context, while dropping support for attachment download in IE --- CHANGELOG.md | 2 + js/privatebin.js | 118 +++++++++++++++++++++++++++++++++------------- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 4 files changed, 89 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef114663..1fb3ae22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,13 @@ * 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) diff --git a/js/privatebin.js b/js/privatebin.js index b6a3226e..53474bd1 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -52,6 +52,31 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ 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 * @@ -409,7 +434,8 @@ jQuery.PrivateBin = (function($, RawDeflate) { element.html().replace( /(((https?|ftp):\/\/[\w?!=&.\/-;#@~%+*-]+(?![\w\s?!&.\/;#~%"=-]>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig, '$1' - ) + ), + purifyHtmlConfig ) ); }; @@ -2536,7 +2562,8 @@ jQuery.PrivateBin = (function($, RawDeflate) { // 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 @@ -2752,6 +2779,34 @@ jQuery.PrivateBin = (function($, RawDeflate) { $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 @@ -2761,44 +2816,39 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ me.setAttachment = function(attachmentData, fileName) { - // data URI format: data:[][;base64], + // data URI format: data:[][;base64], // 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); }; /** @@ -3665,7 +3715,14 @@ jQuery.PrivateBin = (function($, RawDeflate) { for (let i = 0; i < $head.length; ++i) { newDoc.write($head[i].outerHTML); } - newDoc.write('
' + DOMPurify.sanitize(Helper.htmlEntities(paste)) + '
'); + newDoc.write( + '
' +
+                DOMPurify.sanitize(
+                    Helper.htmlEntities(paste),
+                    purifyHtmlConfig
+                ) +
+                '
' + ); newDoc.close(); } @@ -5394,11 +5451,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { // 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 diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 1fc8feb2..4c688784 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -73,7 +73,7 @@ endif; ?> - + diff --git a/tpl/page.php b/tpl/page.php index 74254c8e..ca84e8f6 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -51,7 +51,7 @@ endif; ?> - +