Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Umbraco.Core/Constants-HealthChecks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ public static class Security
/// </summary>
public const string CspHeaderCheck = "https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP";

/// <summary>
/// The documentation link for the imaging HMAC secret key check.
/// </summary>
public const string ImagingHMACSecretKeyCheck =
"https://docs.umbraco.com/umbraco-cms/reference/configuration/imagingsettings";

/// <summary>
/// Contains documentation links for HTTPS health checks.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Umbraco.Core/EmbeddedResources/Lang/en.xml
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,11 @@
</key>
<key alias="notificationEmailsCheckSuccessMessage"><![CDATA[Notification email has been set to <strong>%0%</strong>.]]></key>
<key alias="notificationEmailsCheckErrorMessage"><![CDATA[Notification email is still set to the default value of <strong>%0%</strong>.]]></key>
<key alias="imagingHMACSecretKeyCheckSuccessMessage">The HMAC secret key for image URL signing is configured.</key>
<key alias="imagingHMACSecretKeyCheckWarningMessage">
<![CDATA[No HMAC secret key is configured for image URL signing. It is recommended to set
<strong>Umbraco:CMS:Imaging:HMACSecretKey</strong> to prevent unauthorized image manipulation requests.]]>
</key>
<key alias="scheduledHealthCheckEmailBody"><![CDATA[<html><body><p>Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:</p>%2%</body></html>]]></key>
<key alias="scheduledHealthCheckEmailSubject">Umbraco Health Check Status: %0%</key>
</area>
Expand Down
5 changes: 5 additions & 0 deletions src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@
<![CDATA[Notification email has been set to <strong>%0%</strong>.]]></key>
<key alias="notificationEmailsCheckErrorMessage">
<![CDATA[Notification email is still set to the default value of <strong>%0%</strong>.]]></key>
<key alias="imagingHMACSecretKeyCheckSuccessMessage">The HMAC secret key for image URL signing is configured.</key>
<key alias="imagingHMACSecretKeyCheckWarningMessage">
<![CDATA[No HMAC secret key is configured for image URL signing. It is recommended to set
<strong>Umbraco:CMS:Imaging:HMACSecretKey</strong> to prevent unauthorized image manipulation requests.]]>
</key>
<key alias="scheduledHealthCheckEmailBody">
<![CDATA[<html><body><p>Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:</p>%2%</body></html>]]></key>
<key alias="scheduledHealthCheckEmailSubject">Umbraco Health Check Status: %0%</key>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Health check for the HMAC secret key used to authenticate image manipulation URLs.
/// </summary>
[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;

/// <summary>
/// Initializes a new instance of the <see cref="ImagingHMACSecretKeyCheck" /> class.
/// </summary>
/// <param name="textService">The localized text service.</param>
/// <param name="hmacSecretKeyService">The HMAC secret key service.</param>
public ImagingHMACSecretKeyCheck(
ILocalizedTextService textService,
IHmacSecretKeyService hmacSecretKeyService)
{
_textService = textService;
_hmacSecretKeyService = hmacSecretKeyService;
}

/// <inheritdoc />
public override Task<IEnumerable<HealthCheckStatus>> 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());
}

/// <inheritdoc />
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
=> throw new NotSupportedException("Configuration cannot be automatically fixed.");
}
Original file line number Diff line number Diff line change
@@ -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<ILocalizedTextService>();
mock.Setup(x => x.Localize(
It.IsAny<string?>(),
It.IsAny<string?>(),
It.IsAny<CultureInfo?>(),
It.IsAny<IDictionary<string, string?>>()))
.Returns((string? _, string? alias, CultureInfo? _, IDictionary<string, string?> _) => alias ?? string.Empty);
return mock.Object;
}

[Test]
public async Task GetStatusAsync_WhenHMACKeyIsNotConfigured_ReturnsWarning()
{
var hmacSecretKeyService = Mock.Of<IHmacSecretKeyService>(x => x.HasHmacSecretKey() == false);
var check = new ImagingHMACSecretKeyCheck(MockTextService(), hmacSecretKeyService);

IEnumerable<HealthCheckStatus> 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<IHmacSecretKeyService>(x => x.HasHmacSecretKey() == true);
var check = new ImagingHMACSecretKeyCheck(MockTextService(), hmacSecretKeyService);

IEnumerable<HealthCheckStatus> 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);
});
}
}
Loading