diff --git a/build/templates/UmbracoProject/appsettings.json b/build/templates/UmbracoProject/appsettings.json index d282abc86f1a..99e877812cee 100644 --- a/build/templates/UmbracoProject/appsettings.json +++ b/build/templates/UmbracoProject/appsettings.json @@ -17,6 +17,7 @@ "CMS": { //#if (HasNoNodesViewPath || UseHttpsRedirect) "Global": { + "SanitizeTinyMce": true, //#if (!HasNoNodesViewPath && UseHttpsRedirect) "UseHttps": true //#elseif (UseHttpsRedirect) @@ -25,6 +26,7 @@ //#if (HasNoNodesViewPath) "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE" //#endif + }, //#endif "Hosting": { diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 7799fec5eacd..c88083027488 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -28,6 +28,7 @@ public class GlobalSettings internal const bool StaticDisableElectionForSingleServer = false; internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; internal const string StaticSqlWriteLockTimeOut = "00:00:05"; + internal const bool StaticSanitizeTinyMce = false; /// /// Gets or sets a value for the reserved URLs. @@ -157,6 +158,12 @@ public class GlobalSettings /// public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); + /// + /// Gets a value indicating whether TinyMCE scripting sanitization should be applied + /// + [DefaultValue(StaticSanitizeTinyMce)] + public bool SanitizeTinyMce => StaticSanitizeTinyMce; + /// /// An int value representing the time in milliseconds to lock the database for a write operation /// diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs index eeb291c41faf..a3e861e18041 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs @@ -79,7 +79,7 @@ protected async Task CheckForHeader() var success = false; // Access the site home page and check for the click-jack protection header or meta tag - Uri url = _hostingEnvironment.ApplicationMainUrl; + var url = _hostingEnvironment.ApplicationMainUrl.GetLeftPart(UriPartial.Authority); try { diff --git a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs index 26b638a436d7..f662000cd0b4 100644 --- a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs @@ -23,28 +23,28 @@ public UmbracoTreeSearcherFields(ILocalizationService localizationService) } /// - public IEnumerable GetBackOfficeFields() => _backOfficeFields; + public virtual IEnumerable GetBackOfficeFields() => _backOfficeFields; /// - public IEnumerable GetBackOfficeMembersFields() => _backOfficeMembersFields; + public virtual IEnumerable GetBackOfficeMembersFields() => _backOfficeMembersFields; /// - public IEnumerable GetBackOfficeMediaFields() => _backOfficeMediaFields; + public virtual IEnumerable GetBackOfficeMediaFields() => _backOfficeMediaFields; /// - public IEnumerable GetBackOfficeDocumentFields() => Enumerable.Empty(); + public virtual IEnumerable GetBackOfficeDocumentFields() => Enumerable.Empty(); /// - public ISet GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad; + public virtual ISet GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad; /// - public ISet GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad; + public virtual ISet GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad; /// - public ISet GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad; + public virtual ISet GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad; /// - public ISet GetBackOfficeDocumentFieldsToLoad() + public virtual ISet GetBackOfficeDocumentFieldsToLoad() { var fields = _backOfficeDocumentFieldsToLoad; diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 58f3622e67a8..43723207d39f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -413,6 +413,7 @@ internal async Task> GetServerVariablesAsync() {"showAllowSegmentationForDocumentTypes", false}, {"minimumPasswordLength", _memberPasswordConfigurationSettings.RequiredLength}, {"minimumPasswordNonAlphaNum", _memberPasswordConfigurationSettings.GetMinNonAlphaNumericChars()}, + {"sanitizeTinyMce", _globalSettings.SanitizeTinyMce} } }, { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index b78ac1fdfdd0..828296901393 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -590,35 +590,43 @@ public ActionResult Upload(List file) var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); var tempPath = Path.Combine(root,fileName); - - using (var stream = System.IO.File.Create(tempPath)) - { - formFile.CopyToAsync(stream).GetAwaiter().GetResult(); - } - - if (ext.InvariantEquals("udt")) + if (Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root))) { - model.TempFileName = Path.Combine(root, fileName); + using (var stream = System.IO.File.Create(tempPath)) + { + formFile.CopyToAsync(stream).GetAwaiter().GetResult(); + } - var xd = new XmlDocument + if (ext.InvariantEquals("udt")) { - XmlResolver = null - }; - xd.Load(model.TempFileName); + model.TempFileName = Path.Combine(root, fileName); + + var xd = new XmlDocument + { + XmlResolver = null + }; + xd.Load(model.TempFileName); - model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value; - model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value; + model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value; + model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value; + } + else + { + model.Notifications.Add(new BackOfficeNotification( + _localizedTextService.Localize("speechBubbles","operationFailedHeader"), + _localizedTextService.Localize("media","disallowedFileType"), + NotificationStyle.Warning)); + } } else { model.Notifications.Add(new BackOfficeNotification( - _localizedTextService.Localize("speechBubbles","operationFailedHeader"), - _localizedTextService.Localize("media","disallowedFileType"), + _localizedTextService.Localize("speechBubbles", "operationFailedHeader"), + _localizedTextService.Localize("media", "invalidFileName"), NotificationStyle.Warning)); } - } - + } return model; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index 8a965f2c78d9..113b26d74cbb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -12,6 +12,58 @@ var currentOverlay = null; + /** + * @ngdoc method + * @name umbraco.services.overlayService#open + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay. + * + * @param {object} overlay The rendering options for the overlay. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/default/default.html` if nothing is specified. + * @param {string=} overlay.position The alias of the position of the overlay. Defaults to `center`. + * + * Custom positions can be added by adding a CSS rule for the the underlying CSS rule. Eg. for the position `center`, the corresponding `umb-overlay-center` CSS rule is defined as: + * + *
+         * .umb-overlay.umb-overlay-center {
+         *     position: absolute;
+         *     width: 600px;
+         *     height: auto;
+         *     top: 50%;
+         *     left: 50%;
+         *     transform: translate(-50%,-50%);
+         *     border-radius: 3px;
+         * }
+         * 
+ * @param {string=} overlay.size Sets an alias for the size of the overlay to be opened. If set to `small` (default), an `umb-overlay--small` class name will be appended the the class list of the main overlay element in the DOM. + * + * Umbraco does not support any more sizes by default, but if you wish to introduce a `medium` size, you could do so by adding a CSS rule simlar to: + * + *
+         * .umb-overlay-center.umb-overlay--medium {
+         *     width: 800px;
+         * }
+         * 
+ * @param {booean=} overlay.disableBackdropClick A boolean value indicating whether the click event on the backdrop should be disabled. + * @param {string=} overlay.title The overall title of the overlay. The title will be omitted if not specified. + * @param {string=} overlay.subtitle The sub title of the overlay. The sub title will be omitted if not specified. + * @param {object=} overlay.itemDetails An item that will replace the header of the overlay. + * @param {string=} overlay.itemDetails.icon The icon of the item - eg. `icon-book`. + * @param {string=} overlay.itemDetails.title The title of the item. + * @param {string=} overlay.itemDetails.description Sets the description of the item. * + * @param {string=} overlay.submitButtonLabel The label of the submit button. To support localized values, it's recommended to use the `submitButtonLabelKey` instead. + * @param {string=} overlay.submitButtonLabelKey The key to be used for the submit button label. Defaults to `general_submit` if not specified. + * @param {string=} overlay.submitButtonState The state of the submit button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `init`, `busy", `success`, `error`. + * @param {string=} overlay.submitButtonStyle The styling of the submit button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `success` if not specified specified. + * @param {string=} overlay.hideSubmitButton A boolean value indicating whether the submit button should be hidden. Default is `false`. + * @param {string=} overlay.disableSubmitButton A boolean value indicating whether the submit button should be disabled, preventing the user from submitting the overlay. Default is `false`. + * @param {string=} overlay.closeButtonLabel The label of the close button. To support localized values, it's recommended to use the `closeButtonLabelKey` instead. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the close button label. Defaults to `general_close` if not specified. + * @param {string=} overlay.submit A callback function that is invoked when the user submits the overlay. + * @param {string=} overlay.close A callback function that is invoked when the user closes the overlay. + */ function open(newOverlay) { // prevent two open overlays at the same time @@ -49,6 +101,14 @@ eventsService.emit("appState.overlay", overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#close + * @methodOf umbraco.services.overlayService + * + * @description + * Closes the current overlay. + */ function close() { focusLockService.removeInertAttribute(); @@ -61,6 +121,16 @@ eventsService.emit("appState.overlay", null); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#ysod + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay with an error message. + * + * @param {object} error The error to be shown. + */ function ysod(error) { const overlay = { view: "views/common/overlays/ysod/ysod.html", @@ -72,6 +142,36 @@ open(overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#confirm + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay prompting the user to confirm the overlay. + * + * @param {object} overlay The options for the overlay. + * @param {string=} overlay.confirmType The type of the confirm dialog, which helps define standard styling and labels of the overlay. Supported values are `delete` and `remove`. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified. + * @param {string=} overlay.confirmMessageStyle The styling of the confirm message. If `overlay.confirmType` is `delete`, the fallback value is `danger` - otherwise a message style isn't explicitly specified. + * @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. + * + * If not specified, the fallback value depends on the value specified for the `overlay.confirmType` parameter: + * + * - `delete`: fallback key is `danger` + * - `remove`: fallback key is `primary` + * - anything else: no fallback AKA default button style + * @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. + * + * If not specified, the fallback value depends on the value specified for the `overlay.confirmType` parameter: + * + * - `delete`: fallback key is `actions_delete` + * - `remove`: fallback key is `actions_remove` + * - anything else: fallback is `general_confirm` + * @param {function=} overlay.close A callback function that is invoked when the user closes the overlay. + * @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay. + */ function confirm(overlay) { if (!overlay.closeButtonLabelKey) overlay.closeButtonLabelKey = "general_cancel"; @@ -99,11 +199,45 @@ open(overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#confirmDelete + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay prompting the user to confirm the overlay. The overlay will have styling and labels useful for when the user needs to confirm a delete action. + * + * @param {object} overlay The options for the overlay. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified. + * @param {string=} overlay.confirmMessageStyle The styling of the confirm message. Defaults to `delete` if not specified specified. + * @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `danger` if not specified specified. + * @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. Defaults to `actions_delete` if not specified. + * @param {function=} overlay.close A callback function that is invoked when the user closes the overlay. + * @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay. + */ function confirmDelete(overlay) { overlay.confirmType = "delete"; confirm(overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#confirmRemove + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay prompting the user to confirm the overlay. The overlay will have styling and labels useful for when the user needs to confirm a remove action. + * + * @param {object} overlay The options for the overlay. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified. + * @param {string=} overlay.confirmMessageStyle The styling of the confirm message - eg. `danger`. + * @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `primary` if not specified specified. + * @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. Defaults to `actions_remove` if not specified. + * @param {function=} overlay.close A callback function that is invoked when the user closes the overlay. + * @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay. + */ function confirmRemove(overlay) { overlay.confirmType = "remove"; confirm(overlay); 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 4556641eca76..6c6237263f49 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 @@ -1502,6 +1502,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) { @@ -1513,6 +1526,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.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index d0427cad0a3d..6c1e5058d267 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -55,6 +55,11 @@ input.umb-table__input { color: @ui-disabled-type; } +.umb-table-head__icon { + position: relative; + top: 2px; +} + .umb-table-head__link { background: transparent; border: 0 none; @@ -111,7 +116,7 @@ input.umb-table__input { .umb-table-body .umb-table-row.-selectable { cursor: pointer; } -.umb-table-row.-selected, +.umb-table-row.-selected, .umb-table-body .umb-table-row.-selectable:hover { &::before { content: ""; @@ -226,7 +231,7 @@ input.umb-table__input { &.umb-table-body__checkicon { display: inline-block; } - } + } } // Table Row Styles @@ -309,8 +314,8 @@ input.umb-table__input { .umb-table__loading-overlay { position: absolute; - width: 100%; - height: 100%; + width: 100%; + height: 100%; background-color: rgba(255, 255, 255, 0.7); z-index: 1; } @@ -330,7 +335,7 @@ input.umb-table__input { } .umb-table--condensed { - + .umb-table-cell:first-of-type:not(.not-fixed) { padding-top: 10px; padding-bottom: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index e8f174516c13..77ace4af7004 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -60,20 +60,20 @@ ng-click="vm.selectAll($event)" ng-checked="vm.isSelectedAll()"> -
+
@@ -90,7 +90,7 @@ ng-show="item.isFolder" ng-class="{'-locked': item.selected || !item.file || !item.thumbnail}" ng-click="clickItemName(item, $event, $index)"> - + {{item.name}}
@@ -101,4 +101,3 @@
- diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html index fa146f12f04b..f429c04f1d31 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -22,7 +22,7 @@ - {{ph = placeholder(config);""}} + {{ph = placeholder(config);hasTabsOrFirstRender = (elemTypeTabs[config.ncAlias].length || config.ncAlias=='');""}}