diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 118738bd9e9c..e0b637f39d86 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -126,6 +126,12 @@ public static class Security /// public const string CspHeaderCheck = "https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP"; + /// + /// The documentation link for the imaging HMAC secret key check. + /// + public const string ImagingHMACSecretKeyCheck = + "https://docs.umbraco.com/umbraco-cms/reference/configuration/imagingsettings"; + /// /// Contains documentation links for HTTPS health checks. /// diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 6ff466663419..d1d6dd817c5f 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -509,6 +509,11 @@ %0%.]]> %0%.]]> + The HMAC secret key for image URL signing is configured. + + Umbraco:CMS:Imaging:HMACSecretKey to prevent unauthorized image manipulation requests.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
Umbraco Health Check Status: %0% diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 2514454ba14b..955d2ef7d45c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -498,6 +498,11 @@ %0%.]]> %0%.]]> + The HMAC secret key for image URL signing is configured. + + Umbraco:CMS:Imaging:HMACSecretKey to prevent unauthorized image manipulation requests.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
Umbraco Health Check Status: %0% diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ImagingHMACSecretKeyCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ImagingHMACSecretKeyCheck.cs new file mode 100644 index 000000000000..4c8de6a235bd --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ImagingHMACSecretKeyCheck.cs @@ -0,0 +1,58 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the HMAC secret key used to authenticate image manipulation URLs. +/// +[HealthCheck( + "A3E3A6C8-3D42-4F6E-B285-9F6A5C7E1A14", + "Imaging HMAC Secret Key", + Description = "Verifies that an HMAC secret key is configured to authenticate and protect image manipulation URLs.", + Group = "Security")] +public class ImagingHMACSecretKeyCheck : HealthCheck +{ + private readonly ILocalizedTextService _textService; + private readonly IHmacSecretKeyService _hmacSecretKeyService; + + /// + /// Initializes a new instance of the class. + /// + /// The localized text service. + /// The HMAC secret key service. + public ImagingHMACSecretKeyCheck( + ILocalizedTextService textService, + IHmacSecretKeyService hmacSecretKeyService) + { + _textService = textService; + _hmacSecretKeyService = hmacSecretKeyService; + } + + /// + public override Task> GetStatusAsync() + { + bool isConfigured = _hmacSecretKeyService.HasHmacSecretKey(); + + HealthCheckStatus status = isConfigured + ? new HealthCheckStatus(_textService.Localize("healthcheck", "imagingHMACSecretKeyCheckSuccessMessage")) + { + ResultType = StatusResultType.Success, + } + : new HealthCheckStatus(_textService.Localize("healthcheck", "imagingHMACSecretKeyCheckWarningMessage")) + { + ResultType = StatusResultType.Warning, + ReadMoreLink = Constants.HealthChecks.DocumentationLinks.Security.ImagingHMACSecretKeyCheck, + }; + + return Task.FromResult(status.Yield()); + } + + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new NotSupportedException("Configuration cannot be automatically fixed."); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/HealthChecks/ImagingHMACSecretKeyCheckTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/HealthChecks/ImagingHMACSecretKeyCheckTests.cs new file mode 100644 index 000000000000..cefdebe1a3c0 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/HealthChecks/ImagingHMACSecretKeyCheckTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Globalization; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.HealthChecks.Checks.Security; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.HealthChecks; + +[TestFixture] +public class ImagingHMACSecretKeyCheckTests +{ + // ILocalizedTextService.Localize is called via the extension Localize(area, alias) which delegates to + // Localize(area, alias, CultureInfo, IDictionary). We return the alias so tests can assert on which key was used. + private static ILocalizedTextService MockTextService() + { + var mock = new Mock(); + mock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string? _, string? alias, CultureInfo? _, IDictionary _) => alias ?? string.Empty); + return mock.Object; + } + + [Test] + public async Task GetStatusAsync_WhenHMACKeyIsNotConfigured_ReturnsWarning() + { + var hmacSecretKeyService = Mock.Of(x => x.HasHmacSecretKey() == false); + var check = new ImagingHMACSecretKeyCheck(MockTextService(), hmacSecretKeyService); + + IEnumerable statuses = await check.GetStatusAsync(); + + HealthCheckStatus status = statuses.Single(); + Assert.Multiple(() => + { + Assert.That(status.ResultType, Is.EqualTo(StatusResultType.Warning)); + Assert.That(status.Message, Is.EqualTo("imagingHMACSecretKeyCheckWarningMessage")); + Assert.That(status.ReadMoreLink, Is.Not.Null.And.Not.Empty); + }); + } + + [Test] + public async Task GetStatusAsync_WhenHMACKeyIsConfigured_ReturnsSuccess() + { + var hmacSecretKeyService = Mock.Of(x => x.HasHmacSecretKey() == true); + var check = new ImagingHMACSecretKeyCheck(MockTextService(), hmacSecretKeyService); + + IEnumerable statuses = await check.GetStatusAsync(); + + HealthCheckStatus status = statuses.Single(); + Assert.Multiple(() => + { + Assert.That(status.ResultType, Is.EqualTo(StatusResultType.Success)); + Assert.That(status.Message, Is.EqualTo("imagingHMACSecretKeyCheckSuccessMessage")); + Assert.That(status.ReadMoreLink, Is.Null.Or.Empty); + }); + } +}