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;
+ }
+ }
}
}