From 5f263ad851f622ff0e03923ac398fe71ba697da5 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 4 Jun 2017 14:34:37 +0200 Subject: [PATCH 1/2] Added check for X-Content-Type-Options and refactored existing HTTP header check to use common base class. --- .../Checks/Security/BaseHttpHeaderCheck.cs | 220 ++++++++++++++++++ .../Checks/Security/ClickJackingCheck.cs | 210 +---------------- .../Checks/Security/NoSniffCheck.cs | 15 ++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 4 files changed, 241 insertions(+), 206 deletions(-) create mode 100644 src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs create mode 100644 src/Umbraco.Web/HealthCheck/Checks/Security/NoSniffCheck.cs diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs new file mode 100644 index 0000000000..165d636a6d --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using System.Xml.XPath; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Services; + +namespace Umbraco.Web.HealthCheck.Checks.Security +{ + public abstract class BaseHttpHeaderCheck : HealthCheck + { + private readonly ILocalizedTextService _textService; + + private const string SetHeaderInConfigAction = "setHeaderInConfig"; + + private readonly string _header; + private readonly string _value; + private readonly string _localizedTextPrefix; + private readonly bool _metaTagOptionAvailable; + + public BaseHttpHeaderCheck(HealthCheckContext healthCheckContext, + string header, string value, string localizedTextPrefix, bool metaTagOptionAvailable) : base(healthCheckContext) + { + _textService = healthCheckContext.ApplicationContext.Services.TextService; + + _header = header; + _value = value; + _localizedTextPrefix = localizedTextPrefix; + _metaTagOptionAvailable = metaTagOptionAvailable; + } + + /// + /// Get the status for this health check + /// + /// + public override IEnumerable GetStatus() + { + //return the statuses + return new[] { CheckForHeader() }; + } + + /// + /// Executes the action and returns it's status + /// + /// + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) + { + case SetHeaderInConfigAction: + return SetHeaderInConfig(); + default: + throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist"); + } + } + + protected HealthCheckStatus CheckForHeader() + { + var message = string.Empty; + var success = false; + var url = HealthCheckContext.HttpContext.Request.Url; + + // Access the site home page and check for the click-jack protection header or meta tag + var serverVariables = HealthCheckContext.HttpContext.Request.ServerVariables; + var useSsl = GlobalSettings.UseSSL || serverVariables["SERVER_PORT"] == "443"; + var address = string.Format("http{0}://{1}:{2}", useSsl ? "s" : "", url.Host.ToLower(), url.Port); + var request = WebRequest.Create(address); + request.Method = "GET"; + try + { + var response = request.GetResponse(); + + // Check first for header + success = DoHttpHeadersContainHeader(response); + + // If not found, and available, check for meta-tag + if (success == false && _metaTagOptionAvailable) + { + success = DoMetaTagsContainKeyForHeader(response); + } + + message = success + ? _textService.Localize(string.Format("healthcheck/{0}CheckHeaderFound", _localizedTextPrefix)) + : _textService.Localize(string.Format("healthcheck/{0}CheckHeaderNotFound", _localizedTextPrefix)); + } + catch (Exception ex) + { + message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { address, ex.Message }); + } + + var actions = new List(); + if (success == false) + { + actions.Add(new HealthCheckAction(SetHeaderInConfigAction, Id) + { + Name = _textService.Localize("healthcheck/setHeaderInConfig"), + Description = _textService.Localize(string.Format("healthcheck/{0}SetHeaderInConfigDescription", _localizedTextPrefix)) + }); + } + + return + new HealthCheckStatus(message) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + Actions = actions + }; + } + + private bool DoHttpHeadersContainHeader(WebResponse response) + { + return response.Headers.AllKeys.Contains(_header); + } + + private bool DoMetaTagsContainKeyForHeader(WebResponse response) + { + using (var stream = response.GetResponseStream()) + { + if (stream == null) return false; + using (var reader = new StreamReader(stream)) + { + var html = reader.ReadToEnd(); + var metaTags = ParseMetaTags(html); + return metaTags.ContainsKey(_header); + } + } + } + + private static Dictionary ParseMetaTags(string html) + { + var regex = new Regex("() + .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); + } + + private HealthCheckStatus SetHeaderInConfig() + { + var errorMessage = string.Empty; + var success = SaveHeaderToConfigFile(out errorMessage); + + if (success) + { + return + new HealthCheckStatus(_textService.Localize(string.Format("healthcheck/{0}SetHeaderInConfigSuccess", _localizedTextPrefix))) + { + ResultType = StatusResultType.Success + }; + } + + return + new HealthCheckStatus(_textService.Localize("healthcheck/setHeaderInConfigError", new [] { errorMessage })) + { + ResultType = StatusResultType.Error + }; + } + + private bool SaveHeaderToConfigFile(out string errorMessage) + { + try + { + // There don't look to be any useful classes defined in https://msdn.microsoft.com/en-us/library/system.web.configuration(v=vs.110).aspx + // for working with the customHeaders section, so working with the XML directly. + var configFile = IOHelper.MapPath("~/Web.config"); + var doc = XDocument.Load(configFile); + var systemWebServerElement = doc.XPathSelectElement("/configuration/system.webServer"); + var httpProtocolElement = systemWebServerElement.Element("httpProtocol"); + if (httpProtocolElement == null) + { + httpProtocolElement = new XElement("httpProtocol"); + systemWebServerElement.Add(httpProtocolElement); + } + + var customHeadersElement = httpProtocolElement.Element("customHeaders"); + if (customHeadersElement == null) + { + customHeadersElement = new XElement("customHeaders"); + httpProtocolElement.Add(customHeadersElement); + } + + var removeHeaderElement = customHeadersElement.Elements("remove") + .SingleOrDefault(x => x.Attribute("name") != null && + x.Attribute("name").Value == _value); + if (removeHeaderElement == null) + { + removeHeaderElement = new XElement("remove"); + removeHeaderElement.Add(new XAttribute("name", _header)); + customHeadersElement.Add(removeHeaderElement); + } + + var addHeaderElement = customHeadersElement.Elements("add") + .SingleOrDefault(x => x.Attribute("name") != null && + x.Attribute("name").Value == _header); + if (addHeaderElement == null) + { + addHeaderElement = new XElement("add"); + addHeaderElement.Add(new XAttribute("name", _header)); + addHeaderElement.Add(new XAttribute("value", _value)); + customHeadersElement.Add(addHeaderElement); + } + + doc.Save(configFile); + + errorMessage = string.Empty; + return true; + } + catch (Exception ex) + { + errorMessage = ex.Message; + return false; + } + } + } +} diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs index 54f72dc88a..34ba7bee19 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs @@ -1,217 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using System.Xml.XPath; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Services; - -namespace Umbraco.Web.HealthCheck.Checks.Security +namespace Umbraco.Web.HealthCheck.Checks.Security { [HealthCheck( "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0", "Click-Jacking Protection", Description = "Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.", Group = "Security")] - public class ClickJackingCheck : HealthCheck + public class ClickJackingCheck : BaseHttpHeaderCheck { - private readonly ILocalizedTextService _textService; - - private const string SetFrameOptionsHeaderInConfigActiobn = "setFrameOptionsHeaderInConfig"; - - private const string XFrameOptionsHeader = "X-Frame-Options"; - private const string XFrameOptionsValue = "sameorigin"; // Note can't use "deny" as that would prevent Umbraco itself using IFRAMEs - - public ClickJackingCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext) + public ClickJackingCheck(HealthCheckContext healthCheckContext) + : base(healthCheckContext, "X-Frame-Options", "sameorigin", "clickJacking", true) { - _textService = healthCheckContext.ApplicationContext.Services.TextService; - } - - /// - /// Get the status for this health check - /// - /// - public override IEnumerable GetStatus() - { - //return the statuses - return new[] { CheckForFrameOptionsHeader() }; - } - - /// - /// Executes the action and returns it's status - /// - /// - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - { - switch (action.Alias) - { - case SetFrameOptionsHeaderInConfigActiobn: - return SetFrameOptionsHeaderInConfig(); - default: - throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist"); - } - } - - private HealthCheckStatus CheckForFrameOptionsHeader() - { - var message = string.Empty; - var success = false; - var url = HealthCheckContext.HttpContext.Request.Url; - - // Access the site home page and check for the click-jack protection header or meta tag - var serverVariables = HealthCheckContext.HttpContext.Request.ServerVariables; - var useSsl = GlobalSettings.UseSSL || serverVariables["SERVER_PORT"] == "443"; - var address = string.Format("http{0}://{1}:{2}", useSsl ? "s" : "", url.Host.ToLower(), url.Port); - var request = WebRequest.Create(address); - request.Method = "GET"; - try - { - var response = request.GetResponse(); - - // Check first for header - success = DoHeadersContainFrameOptions(response); - - // If not found, check for meta-tag - if (success == false) - { - success = DoMetaTagsContainFrameOptions(response); - } - - message = success - ? _textService.Localize("healthcheck/clickJackingCheckHeaderFound") - : _textService.Localize("healthcheck/clickJackingCheckHeaderNotFound"); - } - catch (Exception ex) - { - message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { address, ex.Message }); - } - - var actions = new List(); - if (success == false) - { - actions.Add(new HealthCheckAction(SetFrameOptionsHeaderInConfigActiobn, Id) - { - Name = _textService.Localize("healthcheck/clickJackingSetHeaderInConfig"), - Description = _textService.Localize("healthcheck/clickJackingSetHeaderInConfigDescription") - }); - } - - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - Actions = actions - }; - } - - private static bool DoHeadersContainFrameOptions(WebResponse response) - { - return response.Headers.AllKeys.Contains(XFrameOptionsHeader); - } - - private static bool DoMetaTagsContainFrameOptions(WebResponse response) - { - using (var stream = response.GetResponseStream()) - { - if (stream == null) return false; - using (var reader = new StreamReader(stream)) - { - var html = reader.ReadToEnd(); - var metaTags = ParseMetaTags(html); - return metaTags.ContainsKey(XFrameOptionsHeader); - } - } - } - - private static Dictionary ParseMetaTags(string html) - { - var regex = new Regex("() - .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); - } - - private HealthCheckStatus SetFrameOptionsHeaderInConfig() - { - var errorMessage = string.Empty; - var success = SaveHeaderToConfigFile(out errorMessage); - - if (success) - { - return - new HealthCheckStatus(_textService.Localize("healthcheck/clickJackingSetHeaderInConfigSuccess")) - { - ResultType = StatusResultType.Success - }; - } - - return - new HealthCheckStatus(_textService.Localize("healthcheck/clickJackingSetHeaderInConfigError", new [] { errorMessage })) - { - ResultType = StatusResultType.Error - }; - } - - private static bool SaveHeaderToConfigFile(out string errorMessage) - { - try - { - // There don't look to be any useful classes defined in https://msdn.microsoft.com/en-us/library/system.web.configuration(v=vs.110).aspx - // for working with the customHeaders section, so working with the XML directly. - var configFile = IOHelper.MapPath("~/Web.config"); - var doc = XDocument.Load(configFile); - var systemWebServerElement = doc.XPathSelectElement("/configuration/system.webServer"); - var httpProtocolElement = systemWebServerElement.Element("httpProtocol"); - if (httpProtocolElement == null) - { - httpProtocolElement = new XElement("httpProtocol"); - systemWebServerElement.Add(httpProtocolElement); - } - - var customHeadersElement = httpProtocolElement.Element("customHeaders"); - if (customHeadersElement == null) - { - customHeadersElement = new XElement("customHeaders"); - httpProtocolElement.Add(customHeadersElement); - } - - var removeHeaderElement = customHeadersElement.Elements("remove") - .SingleOrDefault(x => x.Attribute("name") != null && - x.Attribute("name").Value == XFrameOptionsHeader); - if (removeHeaderElement == null) - { - removeHeaderElement = new XElement("remove"); - removeHeaderElement.Add(new XAttribute("name", XFrameOptionsHeader)); - customHeadersElement.Add(removeHeaderElement); - } - - var addHeaderElement = customHeadersElement.Elements("add") - .SingleOrDefault(x => x.Attribute("name") != null && - x.Attribute("name").Value == XFrameOptionsHeader); - if (addHeaderElement == null) - { - addHeaderElement = new XElement("add"); - addHeaderElement.Add(new XAttribute("name", XFrameOptionsHeader)); - addHeaderElement.Add(new XAttribute("value", XFrameOptionsValue)); - customHeadersElement.Add(addHeaderElement); - } - - doc.Save(configFile); - - errorMessage = string.Empty; - return true; - } - catch (Exception ex) - { - errorMessage = ex.Message; - return false; - } } } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/NoSniffCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/NoSniffCheck.cs new file mode 100644 index 0000000000..4982d9f272 --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/NoSniffCheck.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Web.HealthCheck.Checks.Security +{ + [HealthCheck( + "1CF27DB3-EFC0-41D7-A1BB-EA912064E071", + "Content/MIME Sniffing Protection", + Description = "Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.", + Group = "Security")] + public class NoSniffCheck : BaseHttpHeaderCheck + { + public NoSniffCheck(HealthCheckContext healthCheckContext) + : base(healthCheckContext, "X-Content-Type-Options", "nosniff", "noSniff", false) + { + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0b5652e509..d6d622315f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -325,6 +325,8 @@ + + From 6861453166e7498527925f292ee12e3097eb9a47 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 4 Jun 2017 14:35:37 +0200 Subject: [PATCH 2/2] Added localised texts and renamed some to less specific names when used across checks --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 11 ++++++++--- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 6 +++--- src/Umbraco.Web.UI/umbraco/config/lang/fr.xml | 6 +++--- src/Umbraco.Web.UI/umbraco/config/lang/nl.xml | 6 +++--- src/Umbraco.Web.UI/umbraco/config/lang/ru.xml | 6 +++--- src/Umbraco.Web.UI/umbraco/config/lang/zh.xml | 6 +++--- .../Checks/Security/ExcessiveHeadersCheck.cs | 10 +++++----- .../HealthCheck/Checks/Security/HttpsCheck.cs | 4 ++-- 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index d99cd8f43f..78326ab051 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1476,7 +1476,7 @@ To manage your website, simply open the Umbraco back office and start adding con Your site certificate was marked as valid. Certificate validation error: '%0%' - Error pinging the URL %0% - '%1%' + Error pinging the URL %0% - '%1%' You are currently %0% viewing the site using the HTTPS scheme. The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. @@ -1516,10 +1516,15 @@ To manage your website, simply open the Umbraco back office and start adding con X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config + Set Header in Config Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% + Could not update web.config file. Error: %0% + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. + A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file.