Files
Umbraco-CMS/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs

144 lines
5.1 KiB
C#
Raw Normal View History

2021-02-03 07:58:42 +01:00
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
2021-02-03 07:58:42 +01:00
using System.Net.Http;
using System.Text.RegularExpressions;
2021-02-03 07:58:42 +01:00
using System.Threading.Tasks;
using Umbraco.Core.Services;
Netcore/feature/healthcheck replaceconfiglogic (#8934) * Started moving to JSON instead of config. Mild refactoring but overall keeping it the same, except for injecting IConfiguration and JSON parsing instead of XML Tests currently unaffected, need to increase coverage * Moved to constants for appsettings keys Moved from IConfiguration to global settings - later to be replaced with IOptions Updated translation messages Installed IOptions ready for new PR Updated to new interface, IConfigurationService * Post-merge fix * Namespace move from Umbraco.Web to Umbraco.Core where appropriate * Renamed abstractsettings (from abstractconfig) Moving out of configservice into the POCO config check * Made the IIsCustomErrors healthcheck as obsolete, as no web.config setting). Investigate reintroducing this check in the .NET Core way (UseDevelopment). Reducing use of abstractsettings as not needed - we don't need a config service to read the config settings anymore as they're all explicit POCOs. Consolidated health-checks in project. * Removed test views that weren't meant to be added * Returned to use of abstractsettings with different use * Moved more health checks into correct folder/namespace, and enum into their own file * Correct namespace * Git history/compare lost due to file move, temporarily moving back to original folder. Will do another PR to move after this * Use existing GetStatus in abstract check for Debug mode * Updating to return to previous logic and putting files back into line * Macro errors returned to previous logic * Reuse abstractsettings class * Swapped order to assist with reviewing PR * Updated to include itempath * Not implemented comment to avoid confusion Implemented NotificationEmailCheck * Changed to IOptionsMonitor as per PR comments. Removed configurationservice as we need to rethink the fixing strategy. Updated logger. Needs to show fix message instead of fixing. Temporary fix for IIS version * Switched to IOptionsMonitor for all * No longer attempts to actually fix header in config. Still need to show suggestions. Co-authored-by: Elitsa Marinovska <elm@umbraco.dk>
2020-10-21 10:29:25 +01:00
using Umbraco.Web;
2021-02-03 07:58:42 +01:00
namespace Umbraco.Core.HealthChecks.Checks.Security
{
2021-02-03 07:58:42 +01:00
/// <summary>
/// Provides a base class for health checks of http header values.
/// </summary>
public abstract class BaseHttpHeaderCheck : HealthCheck
{
private readonly string _header;
private readonly string _value;
private readonly string _localizedTextPrefix;
private readonly bool _metaTagOptionAvailable;
2020-05-07 09:34:16 +02:00
private readonly IRequestAccessor _requestAccessor;
2021-02-03 07:58:42 +01:00
private static HttpClient s_httpClient;
2021-02-03 07:58:42 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="BaseHttpHeaderCheck"/> class.
/// </summary>
Merge remote-tracking branch 'origin/dev-v7' into dev-v8 # Conflicts: # src/SolutionInfo.cs # src/Umbraco.Core/Configuration/UmbracoVersion.cs # src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs # src/Umbraco.Core/Persistence/PetaPoco.cs # src/Umbraco.Core/Scoping/NoScope.cs # src/Umbraco.Core/Scoping/ScopeProvider.cs # src/Umbraco.Core/Services/ContentService.cs # src/Umbraco.Core/Services/IContentService.cs # src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs # src/Umbraco.Tests/UI/LegacyDialogTests.cs # src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js # src/Umbraco.Web.UI/umbraco/config/create/UI.xml # src/Umbraco.Web.UI/umbraco/config/lang/zh.xml # src/Umbraco.Web/BatchedDatabaseServerMessenger.cs # src/Umbraco.Web/Editors/ContentController.cs # src/Umbraco.Web/Editors/MediaTypeController.cs # src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs # src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyModelMapper.cs # src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs # src/Umbraco.Web/Trees/ContentTreeControllerBase.cs # src/Umbraco.Web/umbraco.presentation/umbraco/create/MemberGroupTasks.cs
2018-04-19 23:41:35 +10:00
protected BaseHttpHeaderCheck(
2020-05-07 09:34:16 +02:00
IRequestAccessor requestAccessor,
Merge remote-tracking branch 'origin/dev-v7' into dev-v8 # Conflicts: # src/SolutionInfo.cs # src/Umbraco.Core/Configuration/UmbracoVersion.cs # src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs # src/Umbraco.Core/Persistence/PetaPoco.cs # src/Umbraco.Core/Scoping/NoScope.cs # src/Umbraco.Core/Scoping/ScopeProvider.cs # src/Umbraco.Core/Services/ContentService.cs # src/Umbraco.Core/Services/IContentService.cs # src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs # src/Umbraco.Tests/UI/LegacyDialogTests.cs # src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js # src/Umbraco.Web.UI/umbraco/config/create/UI.xml # src/Umbraco.Web.UI/umbraco/config/lang/zh.xml # src/Umbraco.Web/BatchedDatabaseServerMessenger.cs # src/Umbraco.Web/Editors/ContentController.cs # src/Umbraco.Web/Editors/MediaTypeController.cs # src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs # src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs # src/Umbraco.Web/Models/Mapping/ContentPropertyModelMapper.cs # src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs # src/Umbraco.Web/Trees/ContentTreeControllerBase.cs # src/Umbraco.Web/umbraco.presentation/umbraco/create/MemberGroupTasks.cs
2018-04-19 23:41:35 +10:00
ILocalizedTextService textService,
2021-02-03 07:58:42 +01:00
string header,
string value,
string localizedTextPrefix,
bool metaTagOptionAvailable)
{
2021-02-03 07:58:42 +01:00
LocalizedTextService = textService ?? throw new ArgumentNullException(nameof(textService));
2020-05-07 09:34:16 +02:00
_requestAccessor = requestAccessor;
_header = header;
_value = value;
_localizedTextPrefix = localizedTextPrefix;
_metaTagOptionAvailable = metaTagOptionAvailable;
}
2021-02-03 07:58:42 +01:00
private static HttpClient HttpClient => s_httpClient ??= new HttpClient();
/// <summary>
/// Gets the localized text service.
/// </summary>
protected ILocalizedTextService LocalizedTextService { get; }
/// <summary>
/// Gets a link to an external read more page.
/// </summary>
protected abstract string ReadMoreLink { get; }
/// <summary>
/// Get the status for this health check
/// </summary>
2021-02-03 07:58:42 +01:00
public override async Task<IEnumerable<HealthCheckStatus>> GetStatus() =>
await Task.WhenAll(CheckForHeader());
/// <summary>
/// Executes the action and returns it's status
/// </summary>
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
2021-02-03 07:58:42 +01:00
=> throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist");
2021-02-03 07:58:42 +01:00
/// <summary>
/// The actual health check method.
/// </summary>
protected async Task<HealthCheckStatus> CheckForHeader()
{
2021-02-03 07:58:42 +01:00
string message;
var success = false;
// Access the site home page and check for the click-jack protection header or meta tag
2021-02-03 07:58:42 +01:00
Uri url = _requestAccessor.GetApplicationUrl();
try
{
2021-02-03 07:58:42 +01:00
using HttpResponseMessage response = await HttpClient.GetAsync(url);
// Check first for header
2021-02-03 07:58:42 +01:00
success = HasMatchingHeader(response.Headers.Select(x => x.Key));
// If not found, and available, check for meta-tag
if (success == false && _metaTagOptionAvailable)
{
2021-02-03 07:58:42 +01:00
success = await DoMetaTagsContainKeyForHeader(response);
}
message = success
2021-02-03 07:58:42 +01:00
? LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound")
: LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound");
}
catch (Exception ex)
{
2021-02-03 07:58:42 +01:00
message = LocalizedTextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
}
return
new HealthCheckStatus(message)
{
ResultType = success ? StatusResultType.Success : StatusResultType.Error,
2021-02-03 07:58:42 +01:00
ReadMoreLink = success ? null : ReadMoreLink
};
}
private bool HasMatchingHeader(IEnumerable<string> headerKeys)
2021-02-03 07:58:42 +01:00
=> headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase);
2021-02-03 07:58:42 +01:00
private async Task<bool> DoMetaTagsContainKeyForHeader(HttpResponseMessage response)
{
2021-02-03 07:58:42 +01:00
using (Stream stream = await response.Content.ReadAsStreamAsync())
{
2021-02-03 07:58:42 +01:00
if (stream == null)
{
return false;
}
using (var reader = new StreamReader(stream))
{
var html = reader.ReadToEnd();
2021-02-03 07:58:42 +01:00
Dictionary<string, string> metaTags = ParseMetaTags(html);
return HasMatchingHeader(metaTags.Keys);
}
}
}
private static Dictionary<string, string> ParseMetaTags(string html)
{
var regex = new Regex("<meta http-equiv=\"(.+?)\" content=\"(.+?)\"", RegexOptions.IgnoreCase);
return regex.Matches(html)
.Cast<Match>()
.ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value);
}
}
}