From 23ce69db2fe3b0677a0d8520aa48efce3b1773a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 2 Nov 2021 13:21:44 +0100 Subject: [PATCH] Additional optional sanitization of scripting in TinyMCE (#10653) (cherry picked from commit f68dba7bcb16308af17c5385b8e586165e44b578) --- .../Configuration/GlobalSettings.cs | 21 ++++++ .../Configuration/IGlobalSettings.cs | 5 ++ src/Umbraco.Core/Constants-AppSettings.cs | 7 +- .../src/common/services/tinymce.service.js | 67 +++++++++++++++++++ src/Umbraco.Web.UI/web.Template.config | 1 + .../Editors/BackOfficeServerVariables.cs | 1 + 6 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index c844abe75e49..41e8f633c99b 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -395,6 +395,27 @@ public bool UseHttps } } + /// + /// Returns true if TinyMCE scripting sanitization should be applied + /// + /// + /// The default value is false + /// + public bool SanitizeTinyMce + { + get + { + try + { + return bool.Parse(ConfigurationManager.AppSettings[Constants.AppSettings.SanitizeTinyMce]); + } + catch + { + return false; + } + } + } + /// /// An int value representing the time in milliseconds to lock the database for a write operation /// diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs index 483829f85ff3..2ebab722f001 100644 --- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs @@ -77,5 +77,10 @@ public interface IGlobalSettings /// Gets the write lock timeout. /// int SqlWriteLockTimeOut { get; } + + /// + /// Returns true if TinyMCE scripting sanitization should be applied + /// + bool SanitizeTinyMce { get; } } } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 99ea26b4d698..de7799c1655c 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -109,7 +109,7 @@ public static class AppSettings /// A true or false indicating whether umbraco should force a secure (https) connection to the backoffice. /// public const string UseHttps = "Umbraco.Core.UseHttps"; - + /// /// A true/false value indicating whether the content dashboard should be visible for all user groups. /// @@ -155,6 +155,11 @@ public static class Debug /// An int value representing the time in milliseconds to lock the database for a write operation /// public const string SqlWriteLockTimeOut = "Umbraco.Core.SqlWriteLockTimeOut"; + + /// + /// Returns true if TinyMCE scripting sanitization should be applied + /// + public const string SanitizeTinyMce = "Umbraco.Web.SanitizeTinyMce"; } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 46c047448f88..fe3219e2b7aa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -1491,6 +1491,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); } + + if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){ + /** prevent injecting arbitrary JavaScript execution in on-attributes. */ + const allNodes = Array.prototype.slice.call(args.editor.dom.doc.getElementsByTagName("*")); + allNodes.forEach(node => { + for (var i = 0; i < node.attributes.length; i++) { + if(node.attributes[i].name.indexOf("on") === 0) { + node.removeAttribute(node.attributes[i].name) + } + } + }); + } + }); args.editor.on('init', function (e) { @@ -1502,6 +1515,60 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //enable browser based spell checking args.editor.getBody().setAttribute('spellcheck', true); + + /** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes: + * https://github.com/advisories/GHSA-w7jx-j77m-wp65 + * https://github.com/advisories/GHSA-5vm8-hhgr-jcjp + */ + const uriAttributesToSanitize = ['src', 'href', 'data', 'background', 'action', 'formaction', 'poster', 'xlink:href']; + const parseUri = function() { + // Encapsulated JS logic. + const safeSvgDataUrlElements = [ 'img', 'video' ]; + const scriptUriRegExp = /((java|vb)script|mhtml):/i; + const trimRegExp = /[\s\u0000-\u001F]+/g; + const isInvalidUri = (uri, tagName) => { + if (/^data:image\//i.test(uri)) { + return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri); + } else { + return /^data:/i.test(uri); + } + }; + + return function parseUri(uri, tagName) { + uri = uri.replace(trimRegExp, ''); + try { + // Might throw malformed URI sequence + uri = decodeURIComponent(uri); + } catch (ex) { + // Fallback to non UTF-8 decoder + uri = unescape(uri); + } + + if (scriptUriRegExp.test(uri)) { + return; + } + + if (isInvalidUri(uri, tagName)) { + return; + } + + return uri; + } + }(); + + if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){ + args.editor.serializer.addAttributeFilter(uriAttributesToSanitize, function (nodes) { + nodes.forEach(function(node) { + node.attributes.forEach(function(attr) { + const attrName = attr.name.toLowerCase(); + if(uriAttributesToSanitize.indexOf(attrName) !== -1) { + attr.value = parseUri(attr.value, node.name); + } + }); + }); + }); + } + //start watching the value startWatch(); }); diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index e61c6585ad43..f19ab5d3b638 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -51,6 +51,7 @@ +