Skip to content

Commit f68dba7

Browse files
authored
Additional optional sanitization of scripting in TinyMCE (#10653)
1 parent bd23a5f commit f68dba7

File tree

6 files changed

+101
-1
lines changed

6 files changed

+101
-1
lines changed

src/Umbraco.Core/Configuration/GlobalSettings.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,27 @@ public bool UseHttps
395395
}
396396
}
397397

398+
/// <summary>
399+
/// Returns true if TinyMCE scripting sanitization should be applied
400+
/// </summary>
401+
/// <remarks>
402+
/// The default value is false
403+
/// </remarks>
404+
public bool SanitizeTinyMce
405+
{
406+
get
407+
{
408+
try
409+
{
410+
return bool.Parse(ConfigurationManager.AppSettings[Constants.AppSettings.SanitizeTinyMce]);
411+
}
412+
catch
413+
{
414+
return false;
415+
}
416+
}
417+
}
418+
398419
/// <summary>
399420
/// An int value representing the time in milliseconds to lock the database for a write operation
400421
/// </summary>

src/Umbraco.Core/Configuration/IGlobalSettings.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,10 @@ public interface IGlobalSettings
7777
/// Gets the write lock timeout.
7878
/// </summary>
7979
int SqlWriteLockTimeOut { get; }
80+
81+
/// <summary>
82+
/// Returns true if TinyMCE scripting sanitization should be applied
83+
/// </summary>
84+
bool SanitizeTinyMce { get; }
8085
}
8186
}

src/Umbraco.Core/Constants-AppSettings.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public static class AppSettings
109109
/// A true or false indicating whether umbraco should force a secure (https) connection to the backoffice.
110110
/// </summary>
111111
public const string UseHttps = "Umbraco.Core.UseHttps";
112-
112+
113113
/// <summary>
114114
/// A true/false value indicating whether the content dashboard should be visible for all user groups.
115115
/// </summary>
@@ -155,6 +155,11 @@ public static class Debug
155155
/// An int value representing the time in milliseconds to lock the database for a write operation
156156
/// </summary>
157157
public const string SqlWriteLockTimeOut = "Umbraco.Core.SqlWriteLockTimeOut";
158+
159+
/// <summary>
160+
/// Returns true if TinyMCE scripting sanitization should be applied
161+
/// </summary>
162+
public const string SanitizeTinyMce = "Umbraco.Web.SanitizeTinyMce";
158163
}
159164
}
160165
}

src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
14971497
});
14981498

14991499
}
1500+
1501+
if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){
1502+
/** prevent injecting arbitrary JavaScript execution in on-attributes. */
1503+
const allNodes = Array.prototype.slice.call(args.editor.dom.doc.getElementsByTagName("*"));
1504+
allNodes.forEach(node => {
1505+
for (var i = 0; i < node.attributes.length; i++) {
1506+
if(node.attributes[i].name.indexOf("on") === 0) {
1507+
node.removeAttribute(node.attributes[i].name)
1508+
}
1509+
}
1510+
});
1511+
}
1512+
15001513
});
15011514

15021515
args.editor.on('init', function (e) {
@@ -1508,6 +1521,60 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
15081521
//enable browser based spell checking
15091522
args.editor.getBody().setAttribute('spellcheck', true);
15101523

1524+
1525+
/** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes:
1526+
* https://github.com/advisories/GHSA-w7jx-j77m-wp65
1527+
* https://github.com/advisories/GHSA-5vm8-hhgr-jcjp
1528+
*/
1529+
const uriAttributesToSanitize = ['src', 'href', 'data', 'background', 'action', 'formaction', 'poster', 'xlink:href'];
1530+
const parseUri = function() {
1531+
// Encapsulated JS logic.
1532+
const safeSvgDataUrlElements = [ 'img', 'video' ];
1533+
const scriptUriRegExp = /((java|vb)script|mhtml):/i;
1534+
const trimRegExp = /[\s\u0000-\u001F]+/g;
1535+
const isInvalidUri = (uri, tagName) => {
1536+
if (/^data:image\//i.test(uri)) {
1537+
return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri);
1538+
} else {
1539+
return /^data:/i.test(uri);
1540+
}
1541+
};
1542+
1543+
return function parseUri(uri, tagName) {
1544+
uri = uri.replace(trimRegExp, '');
1545+
try {
1546+
// Might throw malformed URI sequence
1547+
uri = decodeURIComponent(uri);
1548+
} catch (ex) {
1549+
// Fallback to non UTF-8 decoder
1550+
uri = unescape(uri);
1551+
}
1552+
1553+
if (scriptUriRegExp.test(uri)) {
1554+
return;
1555+
}
1556+
1557+
if (isInvalidUri(uri, tagName)) {
1558+
return;
1559+
}
1560+
1561+
return uri;
1562+
}
1563+
}();
1564+
1565+
if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){
1566+
args.editor.serializer.addAttributeFilter(uriAttributesToSanitize, function (nodes) {
1567+
nodes.forEach(function(node) {
1568+
node.attributes.forEach(function(attr) {
1569+
const attrName = attr.name.toLowerCase();
1570+
if(uriAttributesToSanitize.indexOf(attrName) !== -1) {
1571+
attr.value = parseUri(attr.value, node.name);
1572+
}
1573+
});
1574+
});
1575+
});
1576+
}
1577+
15111578
//start watching the value
15121579
startWatch();
15131580
});

src/Umbraco.Web.UI/web.Template.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
<add key="Umbraco.ModelsBuilder.Enable" value="true" />
5252
<add key="Umbraco.ModelsBuilder.ModelsMode" value="PureLive" />
5353
<add key="Umbraco.Web.PublishedCache.NuCache.Serializer" value="MsgPack"/>
54+
<add key="Umbraco.Web.SanitizeTinyMce" value="true" />
5455
</appSettings>
5556

5657
<!--

src/Umbraco.Web/Editors/BackOfficeServerVariables.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ internal Dictionary<string, object> GetServerVariables()
361361
{"showAllowSegmentationForDocumentTypes", false},
362362
{"minimumPasswordLength", userMembershipProvider.MinRequiredPasswordLength},
363363
{"minimumPasswordNonAlphaNum", userMembershipProvider.MinRequiredNonAlphanumericCharacters},
364+
{"sanitizeTinyMce", Current.Configs.Global().SanitizeTinyMce}
364365
}
365366
},
366367
{

0 commit comments

Comments
 (0)