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 @@ + +