diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index d5b02db6f2..8d3f9edc0f 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -73,6 +73,10 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - checkout: self + submodules: false + lfs: false, + fetchDepth: 500 - task: NodeTool@0 displayName: Use Node.js $(nodeVersion) retryCountOnTaskFailure: 3 @@ -198,6 +202,11 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - checkout: self + submodules: false + lfs: false, + fetchDepth: 1 + fetchFilter: tree:0 - task: NodeTool@0 displayName: Use Node.js 10.15.x retryCountOnTaskFailure: 3 @@ -249,6 +258,11 @@ stages: pool: vmImage: $(vmImage) steps: + - checkout: self + submodules: false + lfs: false, + fetchDepth: 1 + fetchFilter: tree:0 - task: DownloadPipelineArtifact@2 displayName: Download build artifacts inputs: @@ -288,6 +302,11 @@ stages: variables: Tests__Database__DatabaseType: 'Sqlite' steps: + - checkout: self + submodules: false + lfs: false, + fetchDepth: 1 + fetchFilter: tree:0 # Setup test environment - task: DownloadPipelineArtifact@2 displayName: Download build artifacts diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 6ab88844b7..676e18a589 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -164,6 +165,11 @@ public partial class PreviewController : Controller [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] public ActionResult Frame(int id, string culture) { + if (ValidateProvidedCulture(culture) is false) + { + throw new InvalidOperationException($"Could not recognise the provided culture: {culture}"); + } + EnterPreview(id); // use a numeric URL because content may not be in cache and so .Url would fail @@ -172,6 +178,28 @@ public partial class PreviewController : Controller return RedirectPermanent($"../../{id}{query}"); } + private static bool ValidateProvidedCulture(string culture) + { + if (string.IsNullOrEmpty(culture)) + { + return true; + } + + // We can be confident the backoffice will have provided a valid culture in linking to the + // preview, so we don't need to check that the culture matches an Umbraco language. + // We are only concerned here with protecting against XSS attacks from a fiddled preview + // URL, so we can just confirm we have a valid culture. + try + { + CultureInfo.GetCultureInfo(culture, true); + return true; + } + catch (CultureNotFoundException) + { + return false; + } + } + public ActionResult? EnterPreview(int id) { IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; diff --git a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs index 086a0b0c81..403e6324e1 100644 --- a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs @@ -141,7 +141,10 @@ public abstract class UmbracoViewPage : RazorPage string.Format( ContentSettings.PreviewBadge, HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoPath), - Context.Request.GetEncodedUrl(), + System.Web.HttpUtility.HtmlEncode(Context.Request.GetEncodedUrl()), // Belt and braces - via a browser at least it doesn't seem possible to have anything other than + // a valid culture code provided in the querystring of this URL. + // But just to be sure of prevention of an XSS vulnterablity we'll HTML encode here too. + // An expected URL is untouched by this encoding. UmbracoContext.PublishedRequest?.PublishedContent?.Id); } else diff --git a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js index 8149e45ee8..73ac74847b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js @@ -6,7 +6,7 @@ * @requires angularHelper * * @description - * Promise-based utillity service to lazy-load client-side dependencies inside angular controllers. + * Promise-based utility service to lazy-load client-side dependencies inside angular controllers. * * ##usage * To use, simply inject the assetsService into any controller that needs it, and make @@ -20,10 +20,10 @@ * }); * * - * You can also load individual files, which gives you greater control over what attibutes are passed to the file, as well as timeout + * You can also load individual files, which gives you greater control over what attributes are passed to the file: * *
- *      angular.module("umbraco").controller("my.controller". function(assetsService){
+ *      angular.module("umbraco").controller("my.controller". function(assetsService) {
  *          assetsService.loadJs("script.js", $scope, {charset: 'utf-8'}, 10000 }).then(function(){
  *                 //this code executes when the script is done loading
  *          });
@@ -33,7 +33,7 @@
  * For these cases, there are 2 individual methods, one for javascript, and one for stylesheets:
  *
  * 
- *      angular.module("umbraco").controller("my.controller". function(assetsService){
+ *      angular.module("umbraco").controller("my.controller". function(assetsService) {
  *          assetsService.loadCss("stye.css", $scope, {media: 'print'}, 10000 }).then(function(){
  *                 //loadcss cannot determine when the css is done loading, so this will trigger instantly
  *          });
@@ -55,7 +55,7 @@ angular.module('umbraco.services')
             var _op = (url.indexOf("?") > 0) ? "&" : "?";
             url = url + _op + "umb__rnd=" + rnd;
             return url;
-        };
+        }
 
         function convertVirtualPath(path) {
             //make this work for virtual paths
@@ -72,7 +72,7 @@ angular.module('umbraco.services')
         function getFlatpickrLocales(locales, supportedLocales) {
             return getLocales(locales, supportedLocales, 'lib/flatpickr/l10n/');
         }
-        
+
         function getLocales(locales, supportedLocales, path) {
             var localeUrls = [];
             locales = locales.split(',');
@@ -168,17 +168,13 @@ angular.module('umbraco.services')
              *
              * @param {String} path path to the css file to load
              * @param {Scope} scope optional scope to pass into the loader
-             * @param {Object} keyvalue collection of attributes to pass to the stylesheet element
-             * @param {Number} timeout in milliseconds
              * @returns {Promise} Promise object which resolves when the file has loaded
              */
-            loadCss: function (path, scope, attributes, timeout) {
+            loadCss: function (path, scope) {
 
                 path = convertVirtualPath(path);
 
-                var asset = this._getAssetPromise(path); // $q.defer();
-                var t = timeout || 5000;
-                var a = attributes || undefined;
+                const asset = this._getAssetPromise(path);
 
                 if (asset.state === "new") {
                     asset.state = "loading";
@@ -207,17 +203,13 @@ angular.module('umbraco.services')
              *
              * @param {String} path path to the js file to load
              * @param {Scope} scope optional scope to pass into the loader
-             * @param {Object} keyvalue collection of attributes to pass to the script element
-             * @param {Number} timeout in milliseconds
              * @returns {Promise} Promise object which resolves when the file has loaded
              */
-            loadJs: function (path, scope, attributes, timeout) {
+            loadJs: function (path, scope) {
 
                 path = convertVirtualPath(path);
 
-                var asset = this._getAssetPromise(path); // $q.defer();
-                var t = timeout || 5000;
-                var a = attributes || undefined;
+                const asset = this._getAssetPromise(path);
 
                 if (asset.state === "new") {
                     asset.state = "loading";
@@ -250,7 +242,7 @@ angular.module('umbraco.services')
              *
              * @param {Array} pathArray string array of paths to the files to load
              * @param {Scope} scope optional scope to pass into the loader
-             * @param {string} defaultAssetType optional default asset type used to load assets with no extension 
+             * @param {string} defaultAssetType optional default asset type used to load assets with no extension
              * @returns {Promise} Promise object which resolves when all the files has loaded
              */
             load: function (pathArray, scope, defaultAssetType) {
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs
index be90d8695b..7e1d1f163f 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs
@@ -68,12 +68,8 @@ public class SqlServerSyntaxProviderTests : UmbracoIntegrationTest
         }
 
         Assert.AreEqual(
-            @$"DELETE FROM {t("cmsContentNu")} WHERE {c("nodeId")} IN (SELECT {c("nodeId")} FROM (SELECT DISTINCT cmsContentNu.nodeId
-FROM {t("cmsContentNu")}
-INNER JOIN {t("umbracoNode")}
-ON {t("cmsContentNu")}.{c("nodeId")} = {t("umbracoNode")}.{c("id")}
-WHERE (({t("umbracoNode")}.{c("nodeObjectType")} = @0))) x)".Replace(Environment.NewLine, " ").Replace("\n", " ")
-                .Replace("\r", " "),
+            @$"DELETE FROM {t("cmsContentNu")} WHERE {c("nodeId")} IN (SELECT {c("nodeId")} FROM (SELECT DISTINCT cmsContentNu.nodeId FROM {t("cmsContentNu")} INNER JOIN {t("umbracoNode")} ON {t("cmsContentNu")}.{c("nodeId")} = {t("umbracoNode")}.{c("id")} WHERE (({t("umbracoNode")}.{c("nodeObjectType")} = @0))) x)".Replace(Environment.NewLine, " ")
+                .Replace("\n", " ").Replace("\r", " "),
             sqlOutput.SQL.Replace(Environment.NewLine, " ").Replace("\n", " ").Replace("\r", " "));
 
         Assert.AreEqual(1, sqlOutput.Arguments.Length);
diff --git a/version.json b/version.json
index fbbb158dee..ee882f912b 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
 {
   "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
-  "version": "13.6.0-rc3",
+  "version": "13.7.0-rc",
   "assemblyVersion": {
     "precision": "build"
   },
@@ -9,8 +9,7 @@
     "semVer": 2.0
   },
   "publicReleaseRefSpec": [
-    "^refs/heads/main$",
-    "^refs/heads/release/"
+    "^refs/heads/main$", "^refs/heads/release/"
   ],
   "release": {
     "branchName": "release/{version}",