From c3625aed872e46092b3766df19b9b6fca2a7553a Mon Sep 17 00:00:00 2001 From: Jeavon Leopold Date: Thu, 28 Oct 2021 14:57:26 +0100 Subject: [PATCH 1/9] Fix the basehttpheader so that it's checking the root of the domain instead of /umbraco --- .../Checks/Security/BaseHttpHeaderCheck.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs index fea674e12327..1c5aa308ba99 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs @@ -14,8 +14,8 @@ namespace Umbraco.Web.HealthCheck.Checks.Security { public abstract class BaseHttpHeaderCheck : HealthCheck { - protected IRuntimeState Runtime { get; } - protected ILocalizedTextService TextService { get; } + private readonly ILocalizedTextService _textService; + private readonly IRuntimeState _runtime; private const string SetHeaderInConfigAction = "setHeaderInConfig"; @@ -24,14 +24,14 @@ public abstract class BaseHttpHeaderCheck : HealthCheck private readonly string _localizedTextPrefix; private readonly bool _metaTagOptionAvailable; + protected BaseHttpHeaderCheck( IRuntimeState runtime, ILocalizedTextService textService, string header, string value, string localizedTextPrefix, bool metaTagOptionAvailable) { - Runtime = runtime; - TextService = textService ?? throw new ArgumentNullException(nameof(textService)); - + _runtime = runtime; + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _header = header; _value = value; _localizedTextPrefix = localizedTextPrefix; @@ -70,7 +70,8 @@ protected HealthCheckStatus CheckForHeader() var success = false; // Access the site home page and check for the click-jack protection header or meta tag - var url = Runtime.ApplicationUrl; + var url = _runtime.ApplicationUrl.GetLeftPart(UriPartial.Authority); + var request = WebRequest.Create(url); request.Method = "GET"; try @@ -84,15 +85,16 @@ protected HealthCheckStatus CheckForHeader() if (success == false && _metaTagOptionAvailable) { success = DoMetaTagsContainKeyForHeader(response); + } message = success - ? TextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") - : TextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); + ? _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") + : _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); } catch (Exception ex) { - message = TextService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } var actions = new List(); @@ -100,8 +102,8 @@ protected HealthCheckStatus CheckForHeader() { actions.Add(new HealthCheckAction(SetHeaderInConfigAction, Id) { - Name = TextService.Localize("healthcheck", "setHeaderInConfig"), - Description = TextService.Localize($"healthcheck", $"{_localizedTextPrefix}SetHeaderInConfigDescription") + Name = _textService.Localize("healthcheck", "setHeaderInConfig"), + Description = _textService.Localize($"healthcheck", $"{_localizedTextPrefix}SetHeaderInConfigDescription") }); } @@ -149,14 +151,14 @@ private HealthCheckStatus SetHeaderInConfig() if (success) { return - new HealthCheckStatus(TextService.Localize("healthcheck", _localizedTextPrefix + "SetHeaderInConfigSuccess")) + new HealthCheckStatus(_textService.Localize("healthcheck", _localizedTextPrefix + "SetHeaderInConfigSuccess")) { ResultType = StatusResultType.Success }; } return - new HealthCheckStatus(TextService.Localize("healthcheck", "setHeaderInConfigError", new [] { errorMessage })) + new HealthCheckStatus(_textService.Localize("healthcheck", "setHeaderInConfigError", new [] { errorMessage })) { ResultType = StatusResultType.Error }; From 56e05988f42b7e71aa91bae1313beff029f7fd17 Mon Sep 17 00:00:00 2001 From: Martin Bentancour Date: Fri, 29 Oct 2021 16:00:52 +0200 Subject: [PATCH 2/9] Validate fileName to prevent path traversal --- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 1 + .../Editors/ContentTypeController.cs | 68 +++++++++++-------- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index bf89c758aae4..2d1e7d9109f4 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -323,6 +323,7 @@ or click here to choose files You can drag files here to upload Cannot upload this file, it does not have an approved file type + Cannot upload this file, it does not have a valid file name Max file size is Media root Failed to move media diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 79c113f99dbc..62c4b4f739f0 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -328,6 +328,7 @@ or click here to choose files You can drag files here to upload. Cannot upload this file, it does not have an approved file type + Cannot upload this file, it does not have a valid file name Max file size is Media root Failed to move media diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index c878d58e6cc3..62787fbedb94 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -576,43 +576,53 @@ public async Task Upload() var fileName = file.Headers.ContentDisposition.FileName.Trim(Constants.CharArrays.DoubleQuote); var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); - var destFileName = root + "\\" + fileName; - try - { - // due to a bug before 8.7.0 we didn't delete temp files, so we need to make sure to delete before - // moving else you get errors and the upload fails without a message in the UI (there's a JS error) - if(System.IO.File.Exists(destFileName)) - System.IO.File.Delete(destFileName); - - // renaming the file because MultipartFormDataStreamProvider has created a random fileName instead of using the name from the - // content-disposition for more than 6 years now. Creating a CustomMultipartDataStreamProvider deriving from MultipartFormDataStreamProvider - // seems like a cleaner option, but I'm not sure where to put it and renaming only takes one line of code. - System.IO.File.Move(result.FileData[0].LocalFileName, destFileName); - } - catch (Exception ex) + var destFileName = Path.Combine(root, fileName); + if (Path.GetFullPath(destFileName).StartsWith(Path.GetFullPath(root))) { - Logger.Error(ex, "Error uploading udt file to App_Data: {File}", destFileName); - } - - if (ext.InvariantEquals("udt")) - { - model.TempFileName = Path.Combine(root, fileName); + try + { + // due to a bug before 8.7.0 we didn't delete temp files, so we need to make sure to delete before + // moving else you get errors and the upload fails without a message in the UI (there's a JS error) + if(System.IO.File.Exists(destFileName)) + System.IO.File.Delete(destFileName); + + // renaming the file because MultipartFormDataStreamProvider has created a random fileName instead of using the name from the + // content-disposition for more than 6 years now. Creating a CustomMultipartDataStreamProvider deriving from MultipartFormDataStreamProvider + // seems like a cleaner option, but I'm not sure where to put it and renaming only takes one line of code. + System.IO.File.Move(result.FileData[0].LocalFileName, destFileName); + } + catch (Exception ex) + { + Logger.Error(ex, "Error uploading udt file to App_Data: {File}", destFileName); + } - var xd = new XmlDocument + if (ext.InvariantEquals("udt")) { - XmlResolver = null - }; - xd.Load(model.TempFileName); + model.TempFileName = destFileName; + + 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 Notification( + Services.TextService.Localize("speechBubbles", "operationFailedHeader"), + Services.TextService.Localize("media", "disallowedFileType"), + NotificationStyle.Warning)); + } } else { model.Notifications.Add(new Notification( - Services.TextService.Localize("speechBubbles", "operationFailedHeader"), - Services.TextService.Localize("media", "disallowedFileType"), - NotificationStyle.Warning)); + Services.TextService.Localize("speechBubbles", "operationFailedHeader"), + Services.TextService.Localize("media", "invalidFileName"), + NotificationStyle.Warning)); } return model; From a20915664dc35cbc915e06bd881ac0384329fb79 Mon Sep 17 00:00:00 2001 From: Ibrahim Muhammad Nada Date: Fri, 29 Oct 2021 19:06:00 +0300 Subject: [PATCH 3/9] GitHub issue#11299 fix v8 (#11493) * adding validation * adding localization keys * applying cleaning up notes * fix spaces * revert ex.xml * spaces again * keep it DRY * chenge hasTabsAndFirstRender to hasTabsOrFirstRender Co-authored-by: inada --- .../nestedcontent.doctypepicker.html | 9 +++++++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 19 +++++++++---------- .../Umbraco/config/lang/en_us.xml | 1 + 3 files changed, 17 insertions(+), 12 deletions(-) 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=='');""}}
@@ -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/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 35d7b8862c08..0a2edb727311 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -806,6 +806,7 @@ Avatar for Header system field + Last Updated Blue diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 2003536def7a..16002c80f596 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -815,6 +815,7 @@ Avatar for Header system field + Last Updated Blue From 7e55c469e3ca9501036b185efd83295a61a2409c Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Sat, 30 Oct 2021 13:33:09 +0200 Subject: [PATCH 5/9] Added extra selector for styling elements in the content --- src/Umbraco.Web.UI.Docs/umb-docs.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css index 850e0d4aa45b..80d1bbbd2a0d 100644 --- a/src/Umbraco.Web.UI.Docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -34,7 +34,8 @@ a:hover { color: rgba(0, 0, 0, .8); } -.content p code { +.content p code, +.content li code { font-size: 85%; font-family: inherit; background-color: #f7f7f9; From 152956bb1ac22b62e8115ebf780db3bce211b4fa Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Sat, 30 Oct 2021 16:00:49 +0200 Subject: [PATCH 6/9] Added ngdocs documentation for overlay.service.js --- .../src/common/services/overlay.service.js | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) 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); From 1553f02b18585c3946f9b146156042f5e1ab80a4 Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 1 Nov 2021 15:50:54 +0100 Subject: [PATCH 7/9] adding unit test for validating language xml files. (#7174) * adding unit test for validating language xml files. * locating the language files by traversing instead of hardcoded path. * debugging null refs. * debugging * Updated method of detecting language folder for XML tests, that should work correctly on the build server. * Path amends to support running test on build server. * Path amends to support running test on build server (2). Co-authored-by: Andy Butland --- .../Configurations/LanguageXmlTests.cs | 51 +++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 2 files changed, 52 insertions(+) create mode 100644 src/Umbraco.Tests/Configurations/LanguageXmlTests.cs diff --git a/src/Umbraco.Tests/Configurations/LanguageXmlTests.cs b/src/Umbraco.Tests/Configurations/LanguageXmlTests.cs new file mode 100644 index 000000000000..19f8c96f2593 --- /dev/null +++ b/src/Umbraco.Tests/Configurations/LanguageXmlTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using NUnit.Framework; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Configurations +{ + [TestFixture] + public class LanguageXmlTests + { + [Test] + public void Can_Load_Language_Xml_Files() + { + var languageDirectory = GetLanguageDirectory(); + var readFilesCount = 0; + var xmlDocument = new XmlDocument(); + foreach (var languageFile in languageDirectory.EnumerateFiles("*.xml")) + { + // Load will throw an exception if the XML isn't valid. + xmlDocument.Load(languageFile.FullName); + readFilesCount++; + } + + // Ensure that at least one file was read. + Assert.AreNotEqual(0, readFilesCount); + } + + private static DirectoryInfo GetLanguageDirectory() + { + var testDirectoryPathParts = Path.GetDirectoryName(TestContext.CurrentContext.TestDirectory) + .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); + var solutionDirectoryPathParts = testDirectoryPathParts + .Take(Array.IndexOf(testDirectoryPathParts, "src") + 1); + var languageFolderPathParts = new List(solutionDirectoryPathParts); + var additionalPathParts = new[] { "Umbraco.Web.UI", "Umbraco", "config", "lang" }; + languageFolderPathParts.AddRange(additionalPathParts); + + // Hack for build-server - when this path is generated in that envrionment it's missing the "src" folder. + // Not sure why, but if it's missing we'll add it in the right place. + if (!languageFolderPathParts.Contains("src")) + { + languageFolderPathParts.Insert(languageFolderPathParts.Count - additionalPathParts.Length, "src"); + } + + return new DirectoryInfo(string.Join(Path.DirectorySeparatorChar.ToString(), languageFolderPathParts)); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c00c67ca1ce7..c77ba6d188c4 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -120,6 +120,7 @@ + From bd23a5fd2d61a7e4f80dfdcfb7c47fd7cf504ea3 Mon Sep 17 00:00:00 2001 From: Callum Whyte Date: Fri, 29 Oct 2021 19:12:32 +0100 Subject: [PATCH 8/9] Making UmbracoTreeSearcherFields virtual for easier overriding --- .../Search/UmbracoTreeSearcherFields.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index 3025f869b4c0..55becb08a05a 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -24,28 +24,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; From f68dba7bcb16308af17c5385b8e586165e44b578 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 9/9] Additional optional sanitization of scripting in TinyMCE (#10653) --- .../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 070504d93232..0e176155af1e 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 @@ -1497,6 +1497,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) { @@ -1508,6 +1521,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 @@ +