diff --git a/example/Controllers/HomeController.cs b/OwaspHeaders.Core.Example/Controllers/HomeController.cs similarity index 85% rename from example/Controllers/HomeController.cs rename to OwaspHeaders.Core.Example/Controllers/HomeController.cs index 51d96af..d940fb6 100644 --- a/example/Controllers/HomeController.cs +++ b/OwaspHeaders.Core.Example/Controllers/HomeController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace example.Controllers; +namespace OwaspHeaders.Core.Example.Controllers; [ApiController] [Route("/")] diff --git a/example/Helpers/RealisticContentSecurityPolicyGenerators.cs b/OwaspHeaders.Core.Example/Helpers/RealisticContentSecurityPolicyGenerators.cs similarity index 99% rename from example/Helpers/RealisticContentSecurityPolicyGenerators.cs rename to OwaspHeaders.Core.Example/Helpers/RealisticContentSecurityPolicyGenerators.cs index 519f632..c863dc1 100644 --- a/example/Helpers/RealisticContentSecurityPolicyGenerators.cs +++ b/OwaspHeaders.Core.Example/Helpers/RealisticContentSecurityPolicyGenerators.cs @@ -2,7 +2,7 @@ using OwaspHeaders.Core.Extensions; using OwaspHeaders.Core.Models; -namespace example.Helpers; +namespace OwaspHeaders.Core.Example.Helpers; /// /// This class is useful for testing the Secure Headers middleware with a set of realistic diff --git a/example/example.csproj b/OwaspHeaders.Core.Example/OwaspHeaders.Core.Example.csproj similarity index 100% rename from example/example.csproj rename to OwaspHeaders.Core.Example/OwaspHeaders.Core.Example.csproj diff --git a/example/Program.cs b/OwaspHeaders.Core.Example/Program.cs similarity index 100% rename from example/Program.cs rename to OwaspHeaders.Core.Example/Program.cs diff --git a/example/Properties/launchSettings.json b/OwaspHeaders.Core.Example/Properties/launchSettings.json similarity index 100% rename from example/Properties/launchSettings.json rename to OwaspHeaders.Core.Example/Properties/launchSettings.json diff --git a/example/appsettings.Development.json b/OwaspHeaders.Core.Example/appsettings.Development.json similarity index 100% rename from example/appsettings.Development.json rename to OwaspHeaders.Core.Example/appsettings.Development.json diff --git a/example/appsettings.json b/OwaspHeaders.Core.Example/appsettings.json similarity index 100% rename from example/appsettings.json rename to OwaspHeaders.Core.Example/appsettings.json diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/CacheControlHeaderOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/CacheControlHeaderOptionsTests.cs new file mode 100644 index 0000000..351ff59 --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/CacheControlHeaderOptionsTests.cs @@ -0,0 +1,116 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class CacheControlHeaderOptionsTests + { + private int _onNextCalledTimes; + private readonly Task _onNextResult = Task.FromResult(0); + private readonly RequestDelegate _onNext; + private readonly DefaultHttpContext _context; + + public CacheControlHeaderOptionsTests() + { + _onNext = _ => + { + Interlocked.Increment(ref _onNextCalledTimes); + return _onNextResult; + }; + _context = new DefaultHttpContext(); + } + + [Fact] + public async Task Invoke_CacheControl_IsPrivate_HeaderIsPresent() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseCacheControl(@private: true).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseCacheControl); + Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); + + _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); + Assert.True(headerValues.Any()); + Assert.Contains("private", headerValues.First()); + Assert.DoesNotContain("no-cache", headerValues.First()); + Assert.DoesNotContain("no-store", headerValues.First()); + } + + [Fact] + public async Task Invoke_CacheControl_MustRevalidate_HeaderIsPresent() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseCacheControl(mustRevalidate: true).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseCacheControl); + Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); + + _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); + Assert.True(headerValues.Any()); + Assert.Contains("must-revalidate", headerValues.First()); + Assert.DoesNotContain("no-cache", headerValues.First()); + Assert.DoesNotContain("no-store", headerValues.First()); + } + + [Fact] + public async Task Invoke_CacheControl_NoCache_HeaderIsPresent() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseCacheControl(noCache: true).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseCacheControl); + Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); + + _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); + Assert.True(headerValues.Any()); + Assert.Contains("no-cache", headerValues.First()); + Assert.DoesNotContain("private", headerValues.First()); + Assert.DoesNotContain("must-revalidate", headerValues.First()); + } + + [Fact] + public async Task Invoke_CacheControl_NoStore_HeaderIsPresent() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseCacheControl(noStore: true).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseCacheControl); + Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); + + _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); + Assert.True(headerValues.Any()); + Assert.Contains("no-store", headerValues.First()); + Assert.DoesNotContain("private", headerValues.First()); + Assert.DoesNotContain("must-revalidate", headerValues.First()); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/ContentSecurityPolicyOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/ContentSecurityPolicyOptionsTests.cs new file mode 100644 index 0000000..cad9aa6 --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/ContentSecurityPolicyOptionsTests.cs @@ -0,0 +1,142 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Enums; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class ContentSecurityPolicyOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UseContentDefaultSecurityPolicyCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentDefaultSecurityPolicy().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + if (headerPresentConfig.UseContentSecurityPolicy) + { + Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + Assert.Equal("script-src 'self';object-src 'self';block-all-mixed-content;upgrade-insecure-requests;", + _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName]); + } + else + { + Assert.False(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + } + } + + [Fact] + public async Task When_UseContentDefaultSecurityPolicyNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseContentSecurityPolicy); + Assert.False(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + } + + [Fact] + public async Task Invoke_ContentSecurityPolicyHeaderName_HeaderIsPresent_WithMultipleCspSandboxTypes() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentSecurityPolicy().Build(); + headerPresentConfig.SetCspSandBox(CspSandboxType.allowForms, CspSandboxType.allowScripts, + CspSandboxType.allowSameOrigin); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + Assert.Equal( + "sandbox allow-forms allow-scripts allow-same-origin;block-all-mixed-content;upgrade-insecure-requests;", + _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName]); + } + + [Fact] + public async Task Invoke_ContentSecurityPolicyReportOnly_HeaderIsPresent_WithMultipleCspSandboxTypes() + { + const string reportUri = "https://localhost:5001/report-uri"; + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentSecurityPolicyReportOnly(reportUri).Build(); + headerPresentConfig.SetCspSandBox(CspSandboxType.allowForms, CspSandboxType.allowScripts, + CspSandboxType.allowSameOrigin); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyReportOnlyHeaderName)); + Assert.Equal($"block-all-mixed-content;upgrade-insecure-requests;report-uri {reportUri};", + _context.Response.Headers[Constants.ContentSecurityPolicyReportOnlyHeaderName]); + } + + [Fact] + public async Task Invoke_ContentSecurityPolicyReportOnly_HeaderIsNotPresent() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseContentSecurityPolicyReportOnly); + Assert.False(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyReportOnlyHeaderName)); + } + + [Fact] + public async Task Invoke_XContentSecurityPolicyHeaderName_HeaderIsPresent() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentSecurityPolicy(useXContentSecurityPolicy: true).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseXContentSecurityPolicy); + Assert.True(_context.Response.Headers.ContainsKey(Constants.XContentSecurityPolicyHeaderName)); + Assert.Equal("block-all-mixed-content;upgrade-insecure-requests;", + _context.Response.Headers[Constants.XContentSecurityPolicyHeaderName]); + + } + + [Fact] + public async Task Invoke_XContentSecurityPolicyHeaderName_HeaderIsNotPresent() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseXContentSecurityPolicy); + Assert.False(_context.Response.Headers.ContainsKey(Constants.XContentSecurityPolicyHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/CrossOriginOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/CrossOriginOptionsTests.cs new file mode 100644 index 0000000..a2398bf --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/CrossOriginOptionsTests.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using OwaspHeaders.Core.Models; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class CrossOriginOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UseCrossOriginResourcePolicyCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = + SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseCrossOriginResourcePolicy().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseCrossOriginResourcePolicy); + Assert.True(_context.Response.Headers.ContainsKey(Constants.CrossOriginResourcePolicyHeaderName)); + Assert.Equal(CrossOriginResourcePolicy.SameOriginValue, + _context.Response.Headers[Constants.CrossOriginResourcePolicyHeaderName]); + } + + [Fact] + public async Task When_UseCrossOriginResourcePolicyNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseCrossOriginResourcePolicy); + Assert.False(_context.Response.Headers.ContainsKey(Constants.CrossOriginResourcePolicyHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/ExpectCtOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/ExpectCtOptionsTests.cs new file mode 100644 index 0000000..ccb7cba --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/ExpectCtOptionsTests.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class ExpectCtOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UseExpectCtCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseExpectCt("https://test.com/report").Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseExpectCt); + Assert.True(_context.Response.Headers.ContainsKey(Constants.ExpectCtHeaderName)); + Assert.Equal(headerPresentConfig.ExpectCt.BuildHeaderValue(), + _context.Response.Headers[Constants.ExpectCtHeaderName]); + } + + [Fact] + public async Task When_UseExpectCtNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseExpectCt); + Assert.False(_context.Response.Headers.ContainsKey(Constants.ExpectCtHeaderName)); + } + + [Fact] + public async Task When_UseExpectCtCalled_HeaderIsPresent_ReportUri_Optional() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseExpectCt(string.Empty).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseExpectCt); + Assert.True(_context.Response.Headers.ContainsKey(Constants.ExpectCtHeaderName)); + Assert.Equal(headerPresentConfig.ExpectCt.BuildHeaderValue(), + _context.Response.Headers[Constants.ExpectCtHeaderName]); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/PermittedCrossDomainPoliciesOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/PermittedCrossDomainPoliciesOptionsTests.cs new file mode 100644 index 0000000..d605d4a --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/PermittedCrossDomainPoliciesOptionsTests.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class PermittedCrossDomainPoliciesOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UsePermittedCrossDomainPoliciesCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UsePermittedCrossDomainPolicies().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UsePermittedCrossDomainPolicy); + Assert.True(_context.Response.Headers.ContainsKey(Constants.PermittedCrossDomainPoliciesHeaderName)); + Assert.Equal("none;", + _context.Response.Headers[Constants.PermittedCrossDomainPoliciesHeaderName]); + } + + [Fact] + public async Task When_UsePermittedCrossDomainPoliciesCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UsePermittedCrossDomainPolicy); + Assert.False(_context.Response.Headers.ContainsKey(Constants.PermittedCrossDomainPoliciesHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/ReferrerPolicyOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/ReferrerPolicyOptionsTests.cs new file mode 100644 index 0000000..aca1cb2 --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/ReferrerPolicyOptionsTests.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class ReferrerPolicyOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UseReferrerPolicyCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseReferrerPolicy().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseReferrerPolicy); + Assert.True(_context.Response.Headers.ContainsKey(Constants.ReferrerPolicyHeaderName)); + Assert.Equal("no-referrer", _context.Response.Headers[Constants.ReferrerPolicyHeaderName]); + } + + [Fact] + public async Task When_UseReferrerPolicyNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseReferrerPolicy); + Assert.False(_context.Response.Headers.ContainsKey(Constants.ReferrerPolicyHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/SecureHeadersTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/SecureHeadersTests.cs new file mode 100644 index 0000000..4367a36 --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/SecureHeadersTests.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Models; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public abstract class SecureHeadersTests + { + internal int _onNextCalledTimes; + internal readonly Task _onNextResult = Task.FromResult(0); + internal readonly RequestDelegate _onNext; + internal readonly DefaultHttpContext _context; + + public SecureHeadersTests() + { + _onNext = _ => + { + Interlocked.Increment(ref _onNextCalledTimes); + return _onNextResult; + }; + _context = new DefaultHttpContext(); + } + + [Fact] + public async Task InvokeWith_NullConfig_ExceptionThrown() + { + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, null); + + var exception = await Record.ExceptionAsync(() => secureHeadersMiddleware.InvokeAsync(_context)); + + Assert.NotNull(exception); + Assert.IsAssignableFrom(exception); + + var argEx = exception as ArgumentException; + Assert.NotNull(argEx); + Assert.Contains(nameof(SecureHeadersMiddlewareConfiguration), exception.Message); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/StrictTransportSecurityOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/StrictTransportSecurityOptionsTests.cs new file mode 100644 index 0000000..2ff619a --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/StrictTransportSecurityOptionsTests.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class StrictTransportSecurityOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UseHstsCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseHsts().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseHsts); + Assert.True(_context.Response.Headers.ContainsKey(Constants.StrictTransportSecurityHeaderName)); + Assert.Equal("max-age=63072000;includeSubDomains", + _context.Response.Headers[Constants.StrictTransportSecurityHeaderName]); + } + + [Fact] + public async Task When_UseHstsNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresetConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresetConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresetConfig.UseHsts); + Assert.False(_context.Response.Headers.ContainsKey(Constants.StrictTransportSecurityHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/XContextTypeOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/XContextTypeOptionsTests.cs new file mode 100644 index 0000000..fd6005b --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/XContextTypeOptionsTests.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class XContextTypeOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UseContentTypeOptionsCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentTypeOptions().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseXContentTypeOptions); + Assert.True(_context.Response.Headers.ContainsKey(Constants.XContentTypeOptionsHeaderName)); + Assert.Equal("nosniff", _context.Response.Headers[Constants.XContentTypeOptionsHeaderName]); + } + + [Fact] + public async Task When_UseContentTypeOptionNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseXContentTypeOptions); + Assert.False(_context.Response.Headers.ContainsKey(Constants.XContentTypeOptionsHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/XFrameOptionsTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/XFrameOptionsTests.cs new file mode 100644 index 0000000..2e51867 --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/XFrameOptionsTests.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class XFrameOptionsTests : SecureHeadersTests + { + [Fact] + public async Task When_UseXFrameOptionsCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseXFrameOptions().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseXFrameOptions); + Assert.True(_context.Response.Headers.ContainsKey(Constants.XFrameOptionsHeaderName)); + Assert.Equal("DENY", _context.Response.Headers[Constants.XFrameOptionsHeaderName]); + } + + [Fact] + public async Task When_UseXFrameOptionsNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseXFrameOptions); + Assert.False(_context.Response.Headers.ContainsKey(Constants.XFrameOptionsHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/XRemovePoweredByOptions.cs b/OwaspHeaders.Core.Tests/CustomHeaders/XRemovePoweredByOptions.cs new file mode 100644 index 0000000..ff294d9 --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/XRemovePoweredByOptions.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class XRemovePoweredByOptions : SecureHeadersTests + { + [Fact] + public async Task When_RemovePoweredByHeaderCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .RemovePoweredByHeader().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.RemoveXPoweredByHeader); + Assert.False(_context.Response.Headers.ContainsKey(Constants.PoweredByHeaderName)); + Assert.False(_context.Response.Headers.ContainsKey(Constants.ServerHeaderName)); + } + + [Fact] + public async Task When_RemovePoweredByHeaderNotCalled_Header_Not_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerPresentConfig.RemoveXPoweredByHeader); + // Am currently running the 2.1.300 Preview 1 build of the SDK + // and the server doesn't seem to add this header. + // Therefore this assert is commented out, as it will always fail + // Assert.True(_context.Response.Headers.ContainsKey(Constants.PoweredByHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/CustomHeaders/XssProtectionOptionTests.cs b/OwaspHeaders.Core.Tests/CustomHeaders/XssProtectionOptionTests.cs new file mode 100644 index 0000000..7e99f4f --- /dev/null +++ b/OwaspHeaders.Core.Tests/CustomHeaders/XssProtectionOptionTests.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.CustomHeaders +{ + public class XssProtectionOptionTests : SecureHeadersTests + { + [Fact] + public async Task When_UseXssProtectionCalled_Header_Is_Present() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseXssProtection().Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(headerPresentConfig.UseXssProtection); + Assert.True(_context.Response.Headers.ContainsKey(Constants.XssProtectionHeaderName)); + Assert.Equal("0", _context.Response.Headers[Constants.XssProtectionHeaderName]); + } + + [Fact] + public async Task When_UseXssProtectionNotCalled_Header_Not_Present() + { + // arrange + var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.False(headerNotPresentConfig.UseXssProtection); + Assert.False(_context.Response.Headers.ContainsKey(Constants.XssProtectionHeaderName)); + } + } +} diff --git a/OwaspHeaders.Core.Tests/HttpContextExtensionsTests/TryAdd.cs b/OwaspHeaders.Core.Tests/HttpContextExtensionsTests/TryAdd.cs new file mode 100644 index 0000000..6fb7b82 --- /dev/null +++ b/OwaspHeaders.Core.Tests/HttpContextExtensionsTests/TryAdd.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.AspNetCore.Http; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.HttpContextExtensionsTests +{ + public class TryAdd + { + private readonly DefaultHttpContext _context = new(); + + [Fact] + public void CanInjectHeader_When_NotPresent() + { + // Arrange + var headerName = Guid.NewGuid().ToString(); + var headerBody = Guid.NewGuid().ToString(); + + // Act + var response = _context.TryAddHeader(headerName, headerBody); + + // Assert + Assert.True(response); + } + + [Fact] + public void DoesntInjectHeader_When_Present() + { + // Arrange + var headerName = Guid.NewGuid().ToString(); + var headerBody = Guid.NewGuid().ToString(); + + _context.Response.Headers.Add(headerName, headerBody); + + // Act + var response = _context.TryAddHeader(headerName, headerBody); + + // Assert + Assert.True(response); + } + } +} diff --git a/OwaspHeaders.Core.Tests/HttpContextExtensionsTests/TryRemove.cs b/OwaspHeaders.Core.Tests/HttpContextExtensionsTests/TryRemove.cs new file mode 100644 index 0000000..7c06cec --- /dev/null +++ b/OwaspHeaders.Core.Tests/HttpContextExtensionsTests/TryRemove.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.AspNetCore.Http; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.HttpContextExtensionsTests +{ + public class TryRemove + { + private readonly DefaultHttpContext _context = new(); + + [Fact] + public void CanRemoveHeader_When_Present() + { + // Arrange + var headerName = Guid.NewGuid().ToString(); + var headerBody = Guid.NewGuid().ToString(); + _context.Response.Headers.Add(headerName, headerBody); + + // Act + var response = _context.TryRemoveHeader(headerName); + + // Assert + Assert.True(response); + } + + [Fact] + public void ReturnsTrue_When_NotPresent() + { + // Arrange + var headerName = Guid.NewGuid().ToString(); + var headerBody = Guid.NewGuid().ToString(); + + // Act + var response = _context.TryRemoveHeader(headerName); + + // Assert + Assert.True(response); + } + } +} diff --git a/tests/tests.csproj b/OwaspHeaders.Core.Tests/OwaspHeaders.Core.Tests.csproj old mode 100755 new mode 100644 similarity index 67% rename from tests/tests.csproj rename to OwaspHeaders.Core.Tests/OwaspHeaders.Core.Tests.csproj index 879ce90..5a33d19 --- a/tests/tests.csproj +++ b/OwaspHeaders.Core.Tests/OwaspHeaders.Core.Tests.csproj @@ -1,10 +1,10 @@  A Unit Test project (using xunit) for the OwaspHeaders.Core project - 3.2.0 + 8.0.0 Jamie Taylor - OwaspHeaders.Core.tests - net6.0 + OwaspHeaders.Core.Tests + net6.0;net7.0 @@ -13,13 +13,12 @@ all runtime; build; native; contentfiles; analyzers - - + + - - - + + \ No newline at end of file diff --git a/OwaspHeaders.Core.Tests/RegressionTests/CspRegressionTests.cs b/OwaspHeaders.Core.Tests/RegressionTests/CspRegressionTests.cs new file mode 100644 index 0000000..74b883c --- /dev/null +++ b/OwaspHeaders.Core.Tests/RegressionTests/CspRegressionTests.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using OwaspHeaders.Core; +using OwaspHeaders.Core.Enums; +using OwaspHeaders.Core.Extensions; +using OwaspHeaders.Core.Models; +using Xunit; + +namespace OwaspHeaders.Core.Tests.RegressionTests +{ + /// + /// This class contains a number of regression tests against bugs which were reported + /// using GitHub issues. Each test will link to the issue in question. Please make sure + /// that these tests still pass whenever making changes to this codebase + /// + public class CspRegressionTests + { + private int _onNextCalledTimes; + private readonly Task _onNextResult = Task.FromResult(0); + private readonly RequestDelegate _onNext; + private readonly DefaultHttpContext _context; + + public CspRegressionTests() + { + _onNext = _ => + { + Interlocked.Increment(ref _onNextCalledTimes); + return _onNextResult; + }; + _context = new DefaultHttpContext(); + } + + /// + /// This test exercises and provides regression against https://github.com/GaProgMan/OwaspHeaders.Core/issues/61 + /// + [Fact] + public async Task ContentSecurityPolicy_Adds_MultipleSameValue() + { + // arrange + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseContentSecurityPolicy() + .SetCspUris( + new List + { + new() + { + CommandType = CspCommandType.Directive, DirectiveOrUri = "self" + }, + new() + { + CommandType = CspCommandType.Uri, DirectiveOrUri = "cdnjs.cloudflare.com" + } + }, CspUriType.Style).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + + var headerValue = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); + Assert.Equal(1, + headerValue.First() + .Split(" ") + .Count(hv => hv.Contains("cdnjs.cloudflare.com", StringComparison.InvariantCultureIgnoreCase))); + } + + /// + /// This test exercises and proves regression against unnecessary spaces being added between the final + /// directive and first URI in a CSP + /// + [Fact] + public async Task ContentSecurityPolicy_Adds_UnnecessarySpace_Between_FinalDirective_And_First_Uri() + { + // arrange + const string targetCsp = "style-src 'self' cdnjs.cloudflare.com;"; + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentSecurityPolicy(blockAllMixedContent: false, upgradeInsecureRequests: false) + .SetCspUris( + // originally PRODUCES: style-src 'self' cdnjs.cloudflare.com; + new List + { + new() { CommandType = CspCommandType.Directive, DirectiveOrUri = "self" }, + new() { CommandType = CspCommandType.Uri, DirectiveOrUri = "cdnjs.cloudflare.com" } + }, CspUriType.Style).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + + var headerStrings = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); + Assert.NotNull(headerStrings); + + var actualCsp = headerStrings.First(); + var actualCharCount = actualCsp.Length; + var targetCharCount = targetCsp.Length; + + Assert.True(string.Equals(targetCsp, actualCsp, StringComparison.InvariantCultureIgnoreCase)); + Assert.Equal(targetCharCount, actualCharCount); + } + + /// + /// This test exercises and proves regression against unnecessary spaces being added after a + /// directive in a generated CSP, when no URIs are provided + /// + [Fact] + public async Task ContentSecurityPolicy_Adds_UnnecessarySpace_After_FinalDirective_When_OnlyDirectivesProvided() + { + // arrange + const string targetCsp = "style-src 'self';"; + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentSecurityPolicy(blockAllMixedContent: false, upgradeInsecureRequests: false) + .SetCspUris( + // originally PRODUCES: style-src 'self' ; + new List + { + new() { CommandType = CspCommandType.Directive, DirectiveOrUri = "self" } + }, CspUriType.Style).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + + var headerStrings = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); + Assert.NotNull(headerStrings); + + var actualCsp = headerStrings.First(); + var actualCharCount = actualCsp.Length; + var targetCharCount = targetCsp.Length; + + Assert.True(string.Equals(targetCsp, actualCsp, StringComparison.InvariantCultureIgnoreCase)); + Assert.Equal(targetCharCount, actualCharCount); + } + + /// + /// This test exercises and proves regression against unnecessary spaces being added before the first + /// URI in a CSP, when no Directives are provided + /// + [Fact] + public async Task ContentSecurityPolicy_Adds_UnnecessarySpace_Before_First_Uri_When_NoDirectivesProvided() + { + // arrange + const string targetCsp = "style-src cdnjs.cloudflare.com;"; + var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() + .UseContentSecurityPolicy(blockAllMixedContent: false, upgradeInsecureRequests: false) + .SetCspUris( + // originally PRODUCES: style-src cdnjs.cloudflare.com; + new List + { + new() { CommandType = CspCommandType.Uri, DirectiveOrUri = "cdnjs.cloudflare.com" } + }, CspUriType.Style).Build(); + var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); + + // act + await secureHeadersMiddleware.InvokeAsync(_context); + + // assert + Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); + + var headerStrings = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); + Assert.NotNull(headerStrings); + + var actualCsp = headerStrings.First(); + var actualCharCount = actualCsp.Length; + var targetCharCount = targetCsp.Length; + + Assert.True(string.Equals(targetCsp, actualCsp, StringComparison.InvariantCultureIgnoreCase)); + Assert.Equal(targetCharCount, actualCharCount); + } + } +} + diff --git a/OwaspHeaders.Core.Tests/StringBuilderExtensionsTests/TrimEndTests.cs b/OwaspHeaders.Core.Tests/StringBuilderExtensionsTests/TrimEndTests.cs new file mode 100644 index 0000000..4338eda --- /dev/null +++ b/OwaspHeaders.Core.Tests/StringBuilderExtensionsTests/TrimEndTests.cs @@ -0,0 +1,33 @@ +using System.Text; +using OwaspHeaders.Core.Extensions; +using Xunit; + +namespace OwaspHeaders.Core.Tests.StringBuilderExtensionsTests +{ + public class TrimEndTests + { + [Fact] + public void TrimEnd_StringBuilder_Is_Null_Return_StringBuilder() + { + // arrange & act + var builder = ((StringBuilder)null).TrimEnd(); + + // assert + Assert.Null(builder); + } + + [Fact] + public void TrimEnd_StringBuilder_Is_Empty_Return_StringBuilder() + { + // arrange + StringBuilder builder = new StringBuilder(); + + // act + builder = builder.TrimEnd(); + + // assert + Assert.NotNull(builder); + Assert.True(builder.Length == 0); + } + } +} diff --git a/tests/tests.sln b/OwaspHeaders.Core.Tests/tests.sln similarity index 100% rename from tests/tests.sln rename to OwaspHeaders.Core.Tests/tests.sln diff --git a/OwaspHeaders.Core.sln b/OwaspHeaders.Core.sln old mode 100755 new mode 100644 index 605b9d5..e26ed5c --- a/OwaspHeaders.Core.sln +++ b/OwaspHeaders.Core.sln @@ -4,9 +4,9 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OwaspHeaders.Core", "src\OwaspHeaders.Core.csproj", "{02A107D0-EDC6-499A-BF33-9F1F3C588065}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tests", "tests\tests.csproj", "{E5626AB0-703E-46F8-92DC-B525D4CEC4E3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OwaspHeaders.Core.Tests", "OwaspHeaders.Core.Tests\OwaspHeaders.Core.Tests.csproj", "{E5626AB0-703E-46F8-92DC-B525D4CEC4E3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example", "example\example.csproj", "{C70B4E0F-87B6-4681-8B49-7A4956AB86B1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OwaspHeaders.Core.Example", "OwaspHeaders.Core.Example\OwaspHeaders.Core.Example.csproj", "{C70B4E0F-87B6-4681-8B49-7A4956AB86B1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index 9425f51..9d9e106 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,17 @@ Please note: this middleware **DOES NOT SUPPORT BLAZOR OR WEBASSEMBLY APPLICATIO ## Tools Required to Build This Repo -- .NET vLatest +- .NET SDKs vLatest + - 6.0 + - 7.0 + - 8.0* - an IDE (VS Code, Rider, or Visual Studio) - [dotnet-format](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format) global tool. That's it. +* = at the time of pushing version 8 of the repo (Dec 2nd, 2023), the .NET 8 SDK binaries are not available for some Linux distributions (such as Fedora). If v8.0 of .NET is not available for your chosen distro, remove the `net8.0` TFM from all csproj files in order to build and run the code. + ## Pull Requests [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) @@ -84,7 +89,8 @@ public static SecureHeadersMiddlewareConfiguration CustomConfiguration() .CreateBuilder() .UseHsts(1200, false) .UseContentDefaultSecurityPolicy() - .UsePermittedCrossDomainPolicies(XPermittedCrossDomainOptionValue.masterOnly) + .UsePermittedCrossDomainPolicy + (XPermittedCrossDomainOptionValue.masterOnly) .UseReferrerPolicy(ReferrerPolicyOptions.sameOrigin) .Build(); } @@ -93,12 +99,14 @@ public static SecureHeadersMiddlewareConfiguration CustomConfiguration() Then consume it in the following manner: ```csharp -app.UseSecureHeadersMiddleware(CustomSecureHeaderExtensions.CustomConfiguration()); +app.UseSecureHeadersMiddleware( + CustomSecureHeaderExtensions.CustomConfiguration() +); ``` #### Testing the Middleware -An example ASP .NET Core application - with the middleware installed - is provided as part of this repo (see the code in the `example` directory). As such, you can run this example application to see the middleware in use. +An example ASP .NET Core application - with the middleware installed - is provided as part of this repo (see the code in the `OwaspHeaders.Core.Example` directory). As such, you can run this example application to see the middleware in use via a provided OpenAPI endpoint - located at `/swagger`. Or you could add the middleware to an existing application and run through the following Run the application, request one of the pages that it serves and view the headers for the page. diff --git a/changelog.md b/changelog.md index 4e60f78..4a786b1 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ This changelog represents all of the major (i.e. breaking) changes made to the O | Major Version Number | Changes | |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 8 | Removed support for ASP .NET Core on .NET Framework workflows; example and test projects now have OwaspHeaders.Core prefix, re-architected some of the test classes | 7 | Added Cross-Origin-Resource-Policy header to list of defaults; simplfied the use of the middleware in Composite Root/Program.cs | | 6 | Removes Expect-CT Header from the list of default headers | | 5 | XSS Protection is now hard-coded to return "0" if enabled | diff --git a/src/Extensions/HttpContextExtensions.cs b/src/Extensions/HttpContextExtensions.cs index 4d2261d..15f3d47 100644 --- a/src/Extensions/HttpContextExtensions.cs +++ b/src/Extensions/HttpContextExtensions.cs @@ -12,7 +12,10 @@ public static bool ResponseContainsHeader(this HttpContext httpContext, string h public static bool TryAddHeader(this HttpContext httpContext, string headerName, string headerValue) { - if (httpContext.ResponseContainsHeader(headerName)) return true; + if (httpContext.ResponseContainsHeader(headerName)) + { + return true; + } try { httpContext.Response.Headers.Add(headerName, headerValue); @@ -33,7 +36,10 @@ public static bool TryAddHeader(this HttpContext httpContext, string headerName, /// public static bool TryRemoveHeader(this HttpContext httpContext, string headerName) { - if (!httpContext.ResponseContainsHeader(headerName)) return true; + if (!httpContext.ResponseContainsHeader(headerName)) + { + return true; + } try { httpContext.Response.Headers.Remove(headerName); diff --git a/src/OwaspHeaders.Core.csproj b/src/OwaspHeaders.Core.csproj old mode 100755 new mode 100644 index 8a5930e..d8e80ba --- a/src/OwaspHeaders.Core.csproj +++ b/src/OwaspHeaders.Core.csproj @@ -1,17 +1,17 @@ An ASP.NET Core Middleware which adds the OWASP recommended HTTP headers for enhanced security. - 7.5.1 + 8.0.0 Jamie Taylor OwaspHeaders.Core - netstandard2.0 + net6.0;net7.0 OwaspHeaders.Core 2.0.0 LICENSE.txt README-NuGet.md - + diff --git a/src/SecureHeadersMiddleware.cs b/src/SecureHeadersMiddleware.cs old mode 100755 new mode 100644 diff --git a/tests/CacheControlHeaderOptionsTests.cs b/tests/CacheControlHeaderOptionsTests.cs deleted file mode 100644 index 9369f68..0000000 --- a/tests/CacheControlHeaderOptionsTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using OwaspHeaders.Core; -using OwaspHeaders.Core.Extensions; -using Xunit; - -namespace tests; - -[ExcludeFromCodeCoverage] -public class CacheControlHeaderOptionsTests -{ - private int _onNextCalledTimes; - private readonly Task _onNextResult = Task.FromResult(0); - private readonly RequestDelegate _onNext; - private readonly DefaultHttpContext _context; - - public CacheControlHeaderOptionsTests() - { - _onNext = _ => - { - Interlocked.Increment(ref _onNextCalledTimes); - return _onNextResult; - }; - _context = new DefaultHttpContext(); - } - - [Fact] - public async Task Invoke_CacheControl_IsPrivate_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseCacheControl(@private: true).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseCacheControl); - Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); - - _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); - Assert.True(headerValues.Any()); - Assert.Contains("private", headerValues.First()); - Assert.DoesNotContain("no-cache", headerValues.First()); - Assert.DoesNotContain("no-store", headerValues.First()); - } - - [Fact] - public async Task Invoke_CacheControl_MustRevalidate_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseCacheControl(mustRevalidate: true).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseCacheControl); - Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); - - _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); - Assert.True(headerValues.Any()); - Assert.Contains("must-revalidate", headerValues.First()); - Assert.DoesNotContain("no-cache", headerValues.First()); - Assert.DoesNotContain("no-store", headerValues.First()); - } - - [Fact] - public async Task Invoke_CacheControl_NoCache_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseCacheControl(noCache: true).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseCacheControl); - Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); - - _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); - Assert.True(headerValues.Any()); - Assert.Contains("no-cache", headerValues.First()); - Assert.DoesNotContain("private", headerValues.First()); - Assert.DoesNotContain("must-revalidate", headerValues.First()); - } - - [Fact] - public async Task Invoke_CacheControl_NoStore_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseCacheControl(noStore: true).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseCacheControl); - Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); - - _context.Response.Headers.TryGetValue(Constants.CacheControlHeaderName, out var headerValues); - Assert.True(headerValues.Any()); - Assert.Contains("no-store", headerValues.First()); - Assert.DoesNotContain("private", headerValues.First()); - Assert.DoesNotContain("must-revalidate", headerValues.First()); - } -} diff --git a/tests/RegressionTests/CspRegressionTests.cs b/tests/RegressionTests/CspRegressionTests.cs deleted file mode 100644 index 944ca33..0000000 --- a/tests/RegressionTests/CspRegressionTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using OwaspHeaders.Core; -using OwaspHeaders.Core.Enums; -using OwaspHeaders.Core.Extensions; -using OwaspHeaders.Core.Models; -using Xunit; - -namespace tests.RegressionTests; - -/// -/// This class contains a number of regression tests against bugs which were reported -/// using GitHub issues. Each test will link to the issue in question. Please make sure -/// that these tests still pass whenever making changes to this codebase -/// -[ExcludeFromCodeCoverage] -public class CspRegressionTests -{ - private int _onNextCalledTimes; - private readonly Task _onNextResult = Task.FromResult(0); - private readonly RequestDelegate _onNext; - private readonly DefaultHttpContext _context; - - public CspRegressionTests() - { - _onNext = _ => - { - Interlocked.Increment(ref _onNextCalledTimes); - return _onNextResult; - }; - _context = new DefaultHttpContext(); - } - - /// - /// This test exercises and provides regression against https://github.com/GaProgMan/OwaspHeaders.Core/issues/61 - /// - [Fact] - public async Task ContentSecurityPolicy_Adds_MultipleSameValue() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseContentSecurityPolicy() - .SetCspUris( - new List - { - new() - { - CommandType = CspCommandType.Directive, DirectiveOrUri = "self" - }, - new() - { - CommandType = CspCommandType.Uri, DirectiveOrUri = "cdnjs.cloudflare.com" - } - }, CspUriType.Style).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - - var headerValue = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); - Assert.Equal(1, - headerValue.First() - .Split(" ") - .Count(hv => hv.Contains("cdnjs.cloudflare.com", StringComparison.InvariantCultureIgnoreCase))); - } - - /// - /// This test exercises and proves regression against unnecessary spaces being added between the final - /// directive and first URI in a CSP - /// - [Fact] - public async Task ContentSecurityPolicy_Adds_UnnecessarySpace_Between_FinalDirective_And_First_Uri() - { - // arrange - const string targetCsp = "style-src 'self' cdnjs.cloudflare.com;"; - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseContentSecurityPolicy(blockAllMixedContent: false, upgradeInsecureRequests: false) - .SetCspUris( - // originally PRODUCES: style-src 'self' cdnjs.cloudflare.com; - new List - { - new() { CommandType = CspCommandType.Directive, DirectiveOrUri = "self" }, - new() { CommandType = CspCommandType.Uri, DirectiveOrUri = "cdnjs.cloudflare.com" } - }, CspUriType.Style).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - - var headerStrings = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); - Assert.NotNull(headerStrings); - - var actualCsp = headerStrings.First(); - var actualCharCount = actualCsp.Length; - var targetCharCount = targetCsp.Length; - - Assert.True(string.Equals(targetCsp, actualCsp, StringComparison.InvariantCultureIgnoreCase)); - Assert.Equal(targetCharCount, actualCharCount); - } - - /// - /// This test exercises and proves regression against unnecessary spaces being added after a - /// directive in a generated CSP, when no URIs are provided - /// - [Fact] - public async Task ContentSecurityPolicy_Adds_UnnecessarySpace_After_FinalDirective_When_OnlyDirectivesProvided() - { - // arrange - const string targetCsp = "style-src 'self';"; - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseContentSecurityPolicy(blockAllMixedContent: false, upgradeInsecureRequests: false) - .SetCspUris( - // originally PRODUCES: style-src 'self' ; - new List - { - new() { CommandType = CspCommandType.Directive, DirectiveOrUri = "self" } - }, CspUriType.Style).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - - var headerStrings = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); - Assert.NotNull(headerStrings); - - var actualCsp = headerStrings.First(); - var actualCharCount = actualCsp.Length; - var targetCharCount = targetCsp.Length; - - Assert.True(string.Equals(targetCsp, actualCsp, StringComparison.InvariantCultureIgnoreCase)); - Assert.Equal(targetCharCount, actualCharCount); - } - - /// - /// This test exercises and proves regression against unnecessary spaces being added before the first - /// URI in a CSP, when no Directives are provided - /// - [Fact] - public async Task ContentSecurityPolicy_Adds_UnnecessarySpace_Before_First_Uri_When_NoDirectivesProvided() - { - // arrange - const string targetCsp = "style-src cdnjs.cloudflare.com;"; - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseContentSecurityPolicy(blockAllMixedContent: false, upgradeInsecureRequests: false) - .SetCspUris( - // originally PRODUCES: style-src cdnjs.cloudflare.com; - new List - { - new() { CommandType = CspCommandType.Uri, DirectiveOrUri = "cdnjs.cloudflare.com" } - }, CspUriType.Style).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - - var headerStrings = _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName].ToList(); - Assert.NotNull(headerStrings); - - var actualCsp = headerStrings.First(); - var actualCharCount = actualCsp.Length; - var targetCharCount = targetCsp.Length; - - Assert.True(string.Equals(targetCsp, actualCsp, StringComparison.InvariantCultureIgnoreCase)); - Assert.Equal(targetCharCount, actualCharCount); - } -} - diff --git a/tests/SecureHeadersInjectedTest.cs b/tests/SecureHeadersInjectedTest.cs deleted file mode 100755 index e6373b0..0000000 --- a/tests/SecureHeadersInjectedTest.cs +++ /dev/null @@ -1,509 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using OwaspHeaders.Core; -using OwaspHeaders.Core.Enums; -using OwaspHeaders.Core.Extensions; -using OwaspHeaders.Core.Models; -using Xunit; - -namespace tests; - -[ExcludeFromCodeCoverage] -public class SecureHeadersInjectedTest -{ - private int _onNextCalledTimes; - private readonly Task _onNextResult = Task.FromResult(0); - private readonly RequestDelegate _onNext; - private readonly DefaultHttpContext _context; - - public SecureHeadersInjectedTest() - { - _onNext = _ => - { - Interlocked.Increment(ref _onNextCalledTimes); - return _onNextResult; - }; - _context = new DefaultHttpContext(); - } - - [Fact] - public async Task Invoke_StrictTransportSecurityHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseHsts().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseHsts); - Assert.True(_context.Response.Headers.ContainsKey(Constants.StrictTransportSecurityHeaderName)); - Assert.Equal("max-age=63072000;includeSubDomains", - _context.Response.Headers[Constants.StrictTransportSecurityHeaderName]); - } - - [Fact] - public async Task Invoke_StrictTransportSecurityHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresetConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresetConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresetConfig.UseHsts); - Assert.False(_context.Response.Headers.ContainsKey(Constants.StrictTransportSecurityHeaderName)); - } - - [Fact] - public async Task Invoke_XFrameOptionsHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseXFrameOptions().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseXFrameOptions); - Assert.True(_context.Response.Headers.ContainsKey(Constants.XFrameOptionsHeaderName)); - Assert.Equal("DENY", _context.Response.Headers[Constants.XFrameOptionsHeaderName]); - } - - [Fact] - public async Task Invoke_XFrameOptionsHeaderName_HeaderIsNotPresentInDefault() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseXFrameOptions); - Assert.False(_context.Response.Headers.ContainsKey(Constants.XFrameOptionsHeaderName)); - } - - [Fact] - public async Task Invoke_XssProtectionHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseXssProtection().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseXssProtection); - Assert.True(_context.Response.Headers.ContainsKey(Constants.XssProtectionHeaderName)); - Assert.Equal("0", _context.Response.Headers[Constants.XssProtectionHeaderName]); - } - - [Fact] - public async Task Invoke_XssProtectionHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseXssProtection); - Assert.False(_context.Response.Headers.ContainsKey(Constants.XssProtectionHeaderName)); - } - - - [Fact] - public async Task Invoke_XContentTypeOptionsHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseContentTypeOptions().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseXContentTypeOptions); - Assert.True(_context.Response.Headers.ContainsKey(Constants.XContentTypeOptionsHeaderName)); - Assert.Equal("nosniff", _context.Response.Headers[Constants.XContentTypeOptionsHeaderName]); - } - - [Fact] - public async Task Invoke_XContentTypeOptionsHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseXContentTypeOptions); - Assert.False(_context.Response.Headers.ContainsKey(Constants.XContentTypeOptionsHeaderName)); - } - - [Fact] - public async Task Invoke_ContentSecurityPolicyHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseContentDefaultSecurityPolicy().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - if (headerPresentConfig.UseContentSecurityPolicy) - { - Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - Assert.Equal("script-src 'self';object-src 'self';block-all-mixed-content;upgrade-insecure-requests;", - _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName]); - } - else - { - Assert.False(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - } - } - - [Fact] - public async Task invoke_NullConfig_ExceptionThrown() - { - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, null); - - var exception = await Record.ExceptionAsync(() => secureHeadersMiddleware.InvokeAsync(_context)); - - Assert.NotNull(exception); - Assert.IsAssignableFrom(exception); - - var argEx = exception as ArgumentException; - Assert.NotNull(argEx); - Assert.Contains(nameof(SecureHeadersMiddlewareConfiguration), exception.Message); - } - - [Fact] - public async Task Invoke_ContentSecurityPolicyHeaderName_HeaderIsPresent_WithMultipleCspSandboxTypes() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseContentSecurityPolicy().Build(); - headerPresentConfig.SetCspSandBox(CspSandboxType.allowForms, CspSandboxType.allowScripts, CspSandboxType.allowSameOrigin); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - Assert.Equal("sandbox allow-forms allow-scripts allow-same-origin;block-all-mixed-content;upgrade-insecure-requests;", - _context.Response.Headers[Constants.ContentSecurityPolicyHeaderName]); - } - - [Fact] - public async Task Invoke_ContentSecurityPolicyHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseContentSecurityPolicy); - Assert.False(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyHeaderName)); - } - - [Fact] - public async Task Invoke_ContentSecurityPolicyReportOnly_HeaderIsPresent_WithMultipleCspSandboxTypes() - { - const string reportUri = "https://localhost:5001/report-uri"; - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseContentSecurityPolicyReportOnly(reportUri).Build(); - headerPresentConfig.SetCspSandBox(CspSandboxType.allowForms, CspSandboxType.allowScripts, CspSandboxType.allowSameOrigin); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyReportOnlyHeaderName)); - Assert.Equal($"block-all-mixed-content;upgrade-insecure-requests;report-uri {reportUri};", - _context.Response.Headers[Constants.ContentSecurityPolicyReportOnlyHeaderName]); - } - - [Fact] - public async Task Invoke_ContentSecurityPolicyReportOnly_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseContentSecurityPolicyReportOnly); - Assert.False(_context.Response.Headers.ContainsKey(Constants.ContentSecurityPolicyReportOnlyHeaderName)); - } - - [Fact] - public async Task Invoke_XContentSecurityPolicyHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseContentSecurityPolicy(useXContentSecurityPolicy: true).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseXContentSecurityPolicy); - Assert.True(_context.Response.Headers.ContainsKey(Constants.XContentSecurityPolicyHeaderName)); - Assert.Equal("block-all-mixed-content;upgrade-insecure-requests;", - _context.Response.Headers[Constants.XContentSecurityPolicyHeaderName]); - - } - - [Fact] - public async Task Invoke_XContentSecurityPolicyHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseXContentSecurityPolicy); - Assert.False(_context.Response.Headers.ContainsKey(Constants.XContentSecurityPolicyHeaderName)); - } - - [Fact] - public async Task Invoke_PermittedCrossDomainPoliciesHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = - SecureHeadersMiddlewareBuilder.CreateBuilder().UsePermittedCrossDomainPolicies().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UsePermittedCrossDomainPolicy); - Assert.True(_context.Response.Headers.ContainsKey(Constants.PermittedCrossDomainPoliciesHeaderName)); - Assert.Equal("none;", - _context.Response.Headers[Constants.PermittedCrossDomainPoliciesHeaderName]); - } - - [Fact] - public async Task Invoke_PermittedCrossDomainPoliciesHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UsePermittedCrossDomainPolicy); - Assert.False(_context.Response.Headers.ContainsKey(Constants.PermittedCrossDomainPoliciesHeaderName)); - } - - [Fact] - public async Task Invoke_ReferrerPolicyHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseReferrerPolicy().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseReferrerPolicy); - Assert.True(_context.Response.Headers.ContainsKey(Constants.ReferrerPolicyHeaderName)); - Assert.Equal("no-referrer", _context.Response.Headers[Constants.ReferrerPolicyHeaderName]); - } - - [Fact] - public async Task Invoke_ReferrerPolicyHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseReferrerPolicy); - Assert.False(_context.Response.Headers.ContainsKey(Constants.ReferrerPolicyHeaderName)); - } - - [Fact] - public async Task Invoke_ExpectCtHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseExpectCt("https://test.com/report").Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseExpectCt); - Assert.True(_context.Response.Headers.ContainsKey(Constants.ExpectCtHeaderName)); - Assert.Equal(headerPresentConfig.ExpectCt.BuildHeaderValue(), - _context.Response.Headers[Constants.ExpectCtHeaderName]); - } - - [Fact] - public async Task Invoke_ExpectCtHeaderName_HeaderIsPresent_ReportUri_Optional() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseExpectCt(string.Empty).Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseExpectCt); - Assert.True(_context.Response.Headers.ContainsKey(Constants.ExpectCtHeaderName)); - Assert.Equal(headerPresentConfig.ExpectCt.BuildHeaderValue(), - _context.Response.Headers[Constants.ExpectCtHeaderName]); - } - - [Fact] - public async Task Invoke_ExpectCtHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseExpectCt); - Assert.False(_context.Response.Headers.ContainsKey(Constants.ExpectCtHeaderName)); - } - - [Fact] - public async Task Invoke_XPoweredByHeader_RemoveHeader() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().RemovePoweredByHeader().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.RemoveXPoweredByHeader); - Assert.False(_context.Response.Headers.ContainsKey(Constants.PoweredByHeaderName)); - Assert.False(_context.Response.Headers.ContainsKey(Constants.ServerHeaderName)); - } - - [Fact] - public async Task Invoke_XPoweredByHeader_DoNotRemoveHeader() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerPresentConfig.RemoveXPoweredByHeader); - // Am currently running the 2.1.300 Preview 1 build of the SDK - // and the server doesn't seem to add this header. - // Therefore this assert is commented out, as it will always fail - //Assert.True(_context.Response.Headers.ContainsKey(Constants.PoweredByHeaderName)); - } - - [Fact] - public async Task Invoke_CacheControl_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder() - .UseCacheControl().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseCacheControl); - Assert.True(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); - Assert.Equal(headerPresentConfig.CacheControl.BuildHeaderValue(), - _context.Response.Headers[Constants.CacheControlHeaderName]); - } - - [Fact] - public async Task Invoke_CacheControl_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder(); - headerNotPresentConfig.UseCacheControl = false; - headerNotPresentConfig.Build(); - - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseCacheControl); - Assert.False(_context.Response.Headers.ContainsKey(Constants.CacheControlHeaderName)); - } - - [Fact] - public async Task Invoke_CrossOriginResourcePolicyHeaderName_HeaderIsPresent() - { - // arrange - var headerPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().UseCrossOriginResourcePolicy().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.True(headerPresentConfig.UseCrossOriginResourcePolicy); - Assert.True(_context.Response.Headers.ContainsKey(Constants.CrossOriginResourcePolicyHeaderName)); - Assert.Equal(CrossOriginResourcePolicy.SameOriginValue, _context.Response.Headers[Constants.CrossOriginResourcePolicyHeaderName]); - } - - [Fact] - public async Task Invoke_CrossOriginResourcePolicyHeaderName_HeaderIsNotPresent() - { - // arrange - var headerNotPresentConfig = SecureHeadersMiddlewareBuilder.CreateBuilder().Build(); - var secureHeadersMiddleware = new SecureHeadersMiddleware(_onNext, headerNotPresentConfig); - - // act - await secureHeadersMiddleware.InvokeAsync(_context); - - // assert - Assert.False(headerNotPresentConfig.UseCrossOriginResourcePolicy); - Assert.False(_context.Response.Headers.ContainsKey(Constants.CrossOriginResourcePolicyHeaderName)); - } -} diff --git a/tests/StringBuilderExtensionTests.cs b/tests/StringBuilderExtensionTests.cs deleted file mode 100644 index c770e2d..0000000 --- a/tests/StringBuilderExtensionTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; -using OwaspHeaders.Core.Extensions; -using Xunit; - -namespace tests; - -[ExcludeFromCodeCoverage] -public class StringBuilderExtensionTests -{ - [Fact] - public void TrimEnd_StringBuilder_Is_Null_Return_StringBuilder() - { - // arrange & act - var builder = ((StringBuilder)null).TrimEnd(); - - // assert - Assert.Null(builder); - } - - [Fact] - public void TrimEnd_StringBuilder_Is_Empty_Return_StringBuilder() - { - // arrange - StringBuilder builder = new StringBuilder(); - - // act - builder = builder.TrimEnd(); - - // assert - Assert.NotNull(builder); - Assert.True(builder.Length == 0); - } -}