diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index a9b70fc4a1..048513a5da 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -86,6 +86,8 @@ namespace JsonSchema public PackageMigrationSettings PackageMigration { get; set; } public LegacyPasswordMigrationSettings LegacyPasswordMigration { get; set; } + + public ContentDashboardSettings ContentDashboard { get; set; } } /// diff --git a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs similarity index 55% rename from src/Umbraco.Core/Configuration/ContentDashboardSettings.cs rename to src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs index 7bef36dba4..3f8546a1ad 100644 --- a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs @@ -1,6 +1,11 @@ - -namespace Umbraco.Cms.Core.Configuration +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models { + /// + /// Typed configuration options for content dashboard settings. + /// + [UmbracoOptions(Constants.Configuration.ConfigContentDashboard)] public class ContentDashboardSettings { private const string DefaultContentDashboardPath = "cms"; @@ -18,6 +23,13 @@ namespace Umbraco.Cms.Core.Configuration /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. /// /// The URL path. - public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; + [DefaultValue(DefaultContentDashboardPath)] + public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; + + /// + /// Gets the allowed addresses to retrieve data for the content dashboard. + /// + /// The URLs. + public string[] ContentDashboardUrlAllowlist { get; set; } } } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 063d733821..ab951618e3 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { public static partial class Constants { @@ -54,6 +54,7 @@ public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; + public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index ce2e4f2304..7bc5ae57c8 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -82,7 +82,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 7b64d05633..955081fa73 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -8,9 +9,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; @@ -40,7 +43,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IDashboardService _dashboardService; private readonly IUmbracoVersion _umbracoVersion; private readonly IShortStringHelper _shortStringHelper; - private readonly IOptions _dashboardSettings; + private readonly ContentDashboardSettings _dashboardSettings; /// /// Initializes a new instance of the with all its dependencies. /// @@ -60,12 +63,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _dashboardService = dashboardService; _umbracoVersion = umbracoVersion; _shortStringHelper = shortStringHelper; - _dashboardSettings = dashboardSettings; + _dashboardSettings = dashboardSettings.Value; } //we have just one instance of HttpClient shared for the entire application private static readonly HttpClient HttpClient = new HttpClient(); + // TODO(V10) : change return type to Task> and consider removing baseUrl as parameter //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side [ValidateAngularAntiForgeryToken] public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.com/") @@ -76,9 +80,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); var isAdmin = user.IsAdmin(); + if (!IsAllowedUrl(baseUrl)) + { + _logger.LogError($"The following URL is not listed in the setting 'Umbraco:CMS:ContentDashboard:ContentDashboardUrlAllowlist' in configuration: {baseUrl}"); + HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + + // Hacking the response - can't set the HttpContext.Response.Body, so instead returning the error as JSON + var errorJson = JsonConvert.SerializeObject(new { Error = "Dashboard source not permitted" }); + return JObject.Parse(errorJson); + } + var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}", baseUrl, - _dashboardSettings.Value.ContentDashboardPath, + _dashboardSettings.ContentDashboardPath, section, allowedSections, language, @@ -116,8 +130,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return result; } + // TODO(V10) : consider removing baseUrl as parameter public async Task GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/") { + if (!IsAllowedUrl(baseUrl)) + { + _logger.LogError($"The following URL is not listed in the setting 'Umbraco:CMS:ContentDashboard:ContentDashboardUrlAllowlist' in configuration: {baseUrl}"); + return BadRequest("Dashboard source not permitted"); + } + var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section); var key = "umbraco-dynamic-dashboard-css-" + section; @@ -152,12 +173,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } - return Content(result,"text/css", Encoding.UTF8); + return Content(result, "text/css", Encoding.UTF8); } public async Task GetRemoteXml(string site, string url) { + if (!IsAllowedUrl(url)) + { + _logger.LogError($"The following URL is not listed in the setting 'Umbraco:CMS:ContentDashboard:ContentDashboardUrlAllowlist' in configuration: {url}"); + return BadRequest("Dashboard source not permitted"); + } + // This is used in place of the old feedproxy.config // Which was used to grab data from our.umbraco.com, umbraco.com or umbraco.tv // for certain dashboards or the help drawer @@ -214,7 +241,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - return Content(result,"text/xml", Encoding.UTF8); + return Content(result, "text/xml", Encoding.UTF8); } @@ -240,5 +267,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers }) }).ToList(); } + + // Checks if the passed URL is part of the configured allowlist of addresses + private bool IsAllowedUrl(string url) + { + // No addresses specified indicates that any URL is allowed + if (_dashboardSettings.ContentDashboardUrlAllowlist is null || _dashboardSettings.ContentDashboardUrlAllowlist.Contains(url, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + else + { + return false; + } + } } }