diff --git a/Readme.md b/Readme.md index 53731bb..f3826b5 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,7 @@ -HardHat is a set of .net core middleware that adds various headers to help protect your site from vulnerablities. Inspired by [helmetJS](https://helmetjs.github.io). Currently in beta, Content Security Policy, Unit tests, documentation due before 1.0.0. Netherless this should work fine. +HardHat is a set of .net core middleware that adds various headers to help protect your site from vulnerabilities. Inspired by [helmetJS](https://helmetjs.github.io). Currently in beta, Content Security Policy, Unit tests, documentation due before 1.0.0. Even still, this should work fine. In short this allows: @@ -22,6 +22,14 @@ In short this allows: app.UseIENoOpen(); // don't allow old ie to open files in the context of your site app.UseNoMimeSniff(); // prevent MIME sniffing https://en.wikipedia.org/wiki/Content_sniffing app.UseCrossSiteScriptingFilters(); //add headers to have the browsers auto detect and block some xss attacks + app.UseContentSecurityPolicy( + new ContentSecurityPolicyBuilder() + .WithDefaultSource(CSPConstants.Self) + .WithImageSource("http://images.mysite.com") + .WithFontSource(CSPConstants.Self) + .WithFrameAncestors(CSPConstants.None) + .BuildPolicy() + ); ... app.UseMvc(routes => { diff --git a/src/HardHat.Example/Startup.cs b/src/HardHat.Example/Startup.cs index 5cea0c3..d1d0f1d 100644 --- a/src/HardHat.Example/Startup.cs +++ b/src/HardHat.Example/Startup.cs @@ -43,14 +43,29 @@ namespace HardHat.Example app.UseExceptionHandler("/Home/Error"); } app.UseDnsPrefetch(allow: false); - app.UseFrameGuard(new FrameGuardOptions(FrameGuardOptions.FrameGuard.SAMEORIGIN)); + app.UseFrameGuard(new FrameGuardOptions("http://amazon.com")); app.UseHsts(maxAge: 5000, includeSubDomains: true, preload: false); app.UseReferrerPolicy(ReferrerPolicy.NoReferrer); app.UseIENoOpen(); app.UseNoMimeSniff(); app.UseCrossSiteScriptingFilters(); - app.UseServerHeader("PoopyServer"); + app.UseContentSecurityPolicy( + new ContentSecurityPolicyBuilder() + .WithDefaultSource(CSPConstants.Self) + .WithImageSource("http://images.mysite.com") + .WithFontSource(CSPConstants.Self) + .WithFrameAncestors(CSPConstants.None) + .WithMediaSource(CSPConstants.Schemes.MediaStream) + .BuildPolicy() + ); app.UseStaticFiles(); + + new ContentSecurityPolicyBuilder() + .WithFontSource(CSPConstants.Self) + .WithImageSource("https://example.com") + .WithSandBox(SandboxOption.AllowForms); + + app.UseMvc(routes => { routes.MapRoute( diff --git a/src/HardHat.UnitTests/ArgumentNulls.cs b/src/HardHat.UnitTests/ArgumentNulls.cs index 5f3a9bf..f05a4aa 100644 --- a/src/HardHat.UnitTests/ArgumentNulls.cs +++ b/src/HardHat.UnitTests/ArgumentNulls.cs @@ -14,6 +14,7 @@ namespace HardHat.UnitTests Assert.Throws(() => new FrameGuard(null, null)); Assert.Throws(() => new ReferrerPolicyMiddleware(null, null)); Assert.Throws(() => new FrameGuardOptions(string.Empty)); + Assert.Throws(() => new ContentSecurityPolicyMiddleware(null, null)); } } } diff --git a/src/HardHat.UnitTests/CSPBuilderTests.cs b/src/HardHat.UnitTests/CSPBuilderTests.cs new file mode 100644 index 0000000..4778054 --- /dev/null +++ b/src/HardHat.UnitTests/CSPBuilderTests.cs @@ -0,0 +1,52 @@ +using HardHat.Builders; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace HardHat.UnitTests +{ + public class CSPBuilderTests + { + [Fact] + public void DefaultBuilderTests() + { + //"default-src 'self' http://*.example.com; " + var builder = ContentSecurityHeaderBuilder.Build(new ContentSecurityPolicy() { + DefaultSrc = new HashSet() { CSPConstants.Self, CSPConstants.None, "http://*.example.com" }, + ScriptSrc = new HashSet() { "http://*.example.com" }, + StyleSrc = new HashSet() { "http://*.example.com" }, + ImgSrc = new HashSet() { "http://*.example.com" }, + ConnectSrc = new HashSet() { "http://*.example.com" }, + FontSrc = new HashSet() { "http://*.example.com" }, + ObjectSrc = new HashSet() { "http://*.example.com" }, + MediaSrc = new HashSet() { "http://*.example.com" }, + ChildSrc = new HashSet() { "http://*.example.com" }, + FormAction = new HashSet() { "http://*.example.com" }, + FrameAncestors = new HashSet() { "http://*.example.com" }, + PluginTypes = new HashSet() { "http://*.example.com" }, + Sandbox = SandboxOption.AllowPointerLock + + }); + Assert.Equal(@"default-src 'self' 'none' http://*.example.com; script-src http://*.example.com; style-src http://*.example.com; img-src http://*.example.com; connect-src http://*.example.com; font-src http://*.example.com; object-src http://*.example.com; media-src http://*.example.com; child-src http://*.example.com; form-action http://*.example.com; frame-ancestors http://*.example.com; sandbox allow-pointer-lock; plugin-types http://*.example.com;", builder); + } + + [Fact] + public void IsReportHeaderSetProperly() + { + var result = ContentSecurityHeaderBuilder.Build(new ContentSecurityPolicy() + { + ReportUri = "/yo", + OnlySendReport = false + }); + Assert.Equal("report-uri /yo;", result); + + var result2 = ContentSecurityHeaderBuilder.Build(new ContentSecurityPolicy() + { + ReportUri = "/yo", + OnlySendReport = true + }); + Assert.Equal("report-uri-Report-Only /yo;", result2); + } + } +} diff --git a/src/HardHat.UnitTests/HardHat.UnitTests.csproj b/src/HardHat.UnitTests/HardHat.UnitTests.csproj index 3085d74..97e4b14 100644 --- a/src/HardHat.UnitTests/HardHat.UnitTests.csproj +++ b/src/HardHat.UnitTests/HardHat.UnitTests.csproj @@ -12,4 +12,7 @@ + + + \ No newline at end of file diff --git a/src/HardHat/Builders/ContentSecurityHeaderBuilder.cs b/src/HardHat/Builders/ContentSecurityHeaderBuilder.cs new file mode 100644 index 0000000..5bc978d --- /dev/null +++ b/src/HardHat/Builders/ContentSecurityHeaderBuilder.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HardHat.Builders +{ + internal class ContentSecurityHeaderBuilder + { + public static string Build(ContentSecurityPolicy policy) + { + var stringBuilder = new StringBuilder(); + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + if (policy.DefaultSrc != null && policy.DefaultSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.DefaultSrc); + stringBuilder.Append($" {string.Join(" ", policy.DefaultSrc)}; "); + } + if (policy.ScriptSrc != null && policy.ScriptSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.ScriptSrc); + stringBuilder.Append($" {string.Join(" ", policy.ScriptSrc)}; "); + } + if (policy.StyleSrc != null && policy.StyleSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.StyleSrc); + stringBuilder.Append($" {string.Join(" ", policy.StyleSrc)}; "); + } + if (policy.ImgSrc != null && policy.ImgSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.ImgSrc); + stringBuilder.Append($" {string.Join(" ", policy.ImgSrc)}; "); + } + if (policy.ConnectSrc != null && policy.ConnectSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.ConnectSrc); + stringBuilder.Append($" {string.Join(" ", policy.ConnectSrc)}; "); + } + if (policy.FontSrc != null && policy.FontSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.FontSrc); + stringBuilder.Append($" {string.Join(" ", policy.FontSrc)}; "); + } + if (policy.ObjectSrc != null && policy.ObjectSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.ObjectSrc); + stringBuilder.Append($" {string.Join(" ", policy.ObjectSrc)}; "); + } + if (policy.MediaSrc != null && policy.MediaSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.MediaSrc); + stringBuilder.Append($" {string.Join(" ", policy.MediaSrc)}; "); + } + if (policy.ChildSrc != null && policy.ChildSrc.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.ChildSrc); + stringBuilder.Append($" {string.Join(" ", policy.ChildSrc)}; "); + } + if (policy.FormAction != null && policy.FormAction.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.FormAction); + stringBuilder.Append($" {string.Join(" ", policy.FormAction)}; "); + } + if (policy.FrameAncestors != null && policy.FrameAncestors.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.FrameAncestors); + stringBuilder.Append($" {string.Join(" ", policy.FrameAncestors)}; "); + } + if (policy.Sandbox != null) + { + stringBuilder.Append(Constants.CSPDirectives.Sandbox); + stringBuilder.Append($" {policy.Sandbox.Value}; "); + } + if (!string.IsNullOrWhiteSpace(policy.ReportUri)) + { + + if (policy.OnlySendReport) + { + stringBuilder.Append(Constants.CSPDirectives.ReportUriReportOnly); + } + else + { + stringBuilder.Append(Constants.CSPDirectives.ReportUri); + } + stringBuilder.Append($" {policy.ReportUri}; "); + } + if(policy.PluginTypes != null && policy.PluginTypes.Count > 0) + { + stringBuilder.Append(Constants.CSPDirectives.PluginTypes); + stringBuilder.Append($" {string.Join(" ", policy.PluginTypes)}; "); + } + return stringBuilder.ToString().TrimEnd(); + } + } +} diff --git a/src/HardHat/CSPConstants.cs b/src/HardHat/CSPConstants.cs new file mode 100644 index 0000000..8af0881 --- /dev/null +++ b/src/HardHat/CSPConstants.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HardHat +{ + public static class CSPConstants + { + internal static string NonceHyphen = "nonce-"; + internal static string sha256 = "sha256-"; + internal static string sha384 = "sha384-"; + internal static string sha512 = "sha512-"; + + /// + /// Refers to the origin from which the protected document is being served, including the same URL scheme and port number. You must include the single quotes. Some browsers specifically exclude blob and filesystem from source directives. Sites needing to allow these content types can specify them using the Data attribute. This can be found at Sources.Scheme.* + /// + public const string Self = @"'self'"; + /// + /// Allows the use of inline resources, such as inline