From 80c137c2c420f84a1006ae4cff266736351bc09f Mon Sep 17 00:00:00 2001 From: Sal Date: Mon, 13 Jul 2020 16:50:21 +0000 Subject: [PATCH 01/26] Initial CSP Middleware commit --- src/Middleware/CSP/ContentSecurityPolicy.cs | 25 +++++++ .../CSP/ContentSecurityPolicyBuilder.cs | 72 +++++++++++++++++++ src/Middleware/CSP/CspMiddleware.cs | 16 +++++ src/Middleware/CSP/LoggingConfiguration.cs | 12 ++++ .../CSP/Microsoft.AspNetCore.Csp.csproj | 16 +++++ src/Middleware/Middleware.sln | 19 ++++- 6 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/CSP/ContentSecurityPolicy.cs create mode 100644 src/Middleware/CSP/ContentSecurityPolicyBuilder.cs create mode 100644 src/Middleware/CSP/CspMiddleware.cs create mode 100644 src/Middleware/CSP/LoggingConfiguration.cs create mode 100644 src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj diff --git a/src/Middleware/CSP/ContentSecurityPolicy.cs b/src/Middleware/CSP/ContentSecurityPolicy.cs new file mode 100644 index 000000000000..26b9e3839c5e --- /dev/null +++ b/src/Middleware/CSP/ContentSecurityPolicy.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Csp +{ + public enum CspMode + { + NONE, + REPORTING, + ENFORCING + } + + public class ContentSecurityPolicy + { + public CspMode CspMode { get; internal set; } + public bool StrictDynamic { get; internal set; } + public bool UnsafeEval { get; internal set; } + public string ReportingUri { get; internal set; } + public LoggingConfiguration LoggingConfiguration { get; internal set; } + public bool ReportOnly { get; internal set; } + } +} diff --git a/src/Middleware/CSP/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/ContentSecurityPolicyBuilder.cs new file mode 100644 index 000000000000..38a69845bc57 --- /dev/null +++ b/src/Middleware/CSP/ContentSecurityPolicyBuilder.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Csp +{ + public class ContentSecurityPolicyBuilder + { + private readonly ContentSecurityPolicy _policy = new ContentSecurityPolicy(); + + public ContentSecurityPolicyBuilder WithCspMode(CspMode cspMode) + { + _policy.CspMode = cspMode; + return this; + } + + public ContentSecurityPolicyBuilder WithStrictDynamic() + { + _policy.StrictDynamic = true; + return this; + } + + public ContentSecurityPolicyBuilder WithUnsafeEval() + { + _policy.UnsafeEval = true; + return this; + } + + public ContentSecurityPolicyBuilder WithReportOnly() + { + _policy.ReportOnly = true; + return this; + } + public ContentSecurityPolicyBuilder WithReportingUri(string reportingUri) + { + // TODO: normalize URL + _policy.ReportingUri = reportingUri; + return this; + } + + public ContentSecurityPolicyBuilder WithLoggingConfiguration(LoggingConfiguration loggingConfiguration) + { + _policy.LoggingConfiguration = loggingConfiguration; + return this; + } + + public ContentSecurityPolicy Build() + { + if (_policy.CspMode == CspMode.NONE) + { + // TODO: Error message + throw new InvalidOperationException(); + } + + if (_policy.ReportOnly && _policy.ReportingUri == null) + { + // TODO: Error message + throw new InvalidOperationException(); + } + + if (_policy.ReportOnly && _policy.LoggingConfiguration == null) + { + // TODO: Error message + throw new InvalidOperationException(); + } + + return _policy; + } + } +} diff --git a/src/Middleware/CSP/CspMiddleware.cs b/src/Middleware/CSP/CspMiddleware.cs new file mode 100644 index 000000000000..f6a52e4176bc --- /dev/null +++ b/src/Middleware/CSP/CspMiddleware.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Csp +{ + public class CspMiddleware + { + + + public Task Invoke(HttpContext context, IContentSecurityPolicyProvider cspProvider) + { + + } + } +} diff --git a/src/Middleware/CSP/LoggingConfiguration.cs b/src/Middleware/CSP/LoggingConfiguration.cs new file mode 100644 index 000000000000..ca7434f680eb --- /dev/null +++ b/src/Middleware/CSP/LoggingConfiguration.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Csp +{ + public class LoggingConfiguration + { + } +} diff --git a/src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj b/src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj new file mode 100644 index 000000000000..1a7ffd8b5504 --- /dev/null +++ b/src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj @@ -0,0 +1,16 @@ + + + + + CSP middleware and policy for ASP.NET Core to enable Content Security Policy. + + $(DefaultNetCoreTargetFramework) + true + aspnetcore;csp + + + + + + + diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index 61aa371867f7..d441580e1261 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -9,7 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{A86E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoApp", "WebSockets\samples\EchoApp\EchoApp.csproj", "{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSockets.TestServer", "WebSockets\samples\TestServer\WebSockets.TestServer.csproj", "{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSockets.TestServer", "WebSockets\samples\TestServer\WebSockets.TestServer.csproj", "{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets", "WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj", "{BECAA6A1-1AA4-415E-ADF3-07C103333826}" EndProject @@ -301,6 +301,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{4623F52E-2 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Microbenchmarks", "perf\ResponseCaching.Microbenchmarks\Microsoft.AspNetCore.ResponseCaching.Microbenchmarks.csproj", "{80C8E810-1206-482E-BE17-961DD2EBFB11}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSP", "CSP", "{75DC8384-0171-47AC-8510-7502E3892A73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Csp", "CSP\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1643,6 +1647,18 @@ Global {80C8E810-1206-482E-BE17-961DD2EBFB11}.Release|x64.Build.0 = Release|Any CPU {80C8E810-1206-482E-BE17-961DD2EBFB11}.Release|x86.ActiveCfg = Release|Any CPU {80C8E810-1206-482E-BE17-961DD2EBFB11}.Release|x86.Build.0 = Release|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Debug|x64.ActiveCfg = Debug|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Debug|x64.Build.0 = Debug|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Debug|x86.ActiveCfg = Debug|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Debug|x86.Build.0 = Debug|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|Any CPU.Build.0 = Release|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x64.ActiveCfg = Release|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x64.Build.0 = Release|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.ActiveCfg = Release|Any CPU + {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1772,6 +1788,7 @@ Global {C4D624B3-749E-41D8-A43B-B304BC3885EA} = {4623F52E-2070-4631-8DEE-7D2F48733FFD} {8A9C1F6C-3A47-4868-AA95-3EBE0260F5A0} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472} {80C8E810-1206-482E-BE17-961DD2EBFB11} = {4623F52E-2070-4631-8DEE-7D2F48733FFD} + {73A2989B-5F4F-4095-AC25-C705173FA825} = {75DC8384-0171-47AC-8510-7502E3892A73} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA} From 591cb47dcbcfe80880084a46e8d2365383e80551 Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 09:12:11 +0000 Subject: [PATCH 02/26] Add test project. Fix project structure. Tests can be built and run after this commit. --- eng/ProjectReferences.props | 1 + eng/SharedFramework.Local.props | 1 + eng/helix/content/RunTests/RunTestsOptions.cs | 20 +- ...ndpointConventionBuilderExtensionsTests.cs | 90 -- .../test/UnitTests/CorsMiddlewareTests.cs | 972 ------------------ .../CORS/test/UnitTests/CorsOptionsTest.cs | 67 -- .../test/UnitTests/CorsPolicyBuilderTests.cs | 432 -------- .../UnitTests/CorsPolicyExtensionsTests.cs | 85 -- .../CORS/test/UnitTests/CorsPolicyTests.cs | 74 -- .../CORS/test/UnitTests/CorsResultTests.cs | 69 -- .../CORS/test/UnitTests/CorsServiceTests.cs | 961 ----------------- .../DefaultCorsPolicyProviderTests.cs | 51 - .../CORS/test/UnitTests/UriHelpersTests.cs | 66 -- src/Middleware/CSP/CSP.slnf | 9 + .../CSP/{ => src}/ContentSecurityPolicy.cs | 0 .../{ => src}/ContentSecurityPolicyBuilder.cs | 0 src/Middleware/CSP/{ => src}/CspMiddleware.cs | 2 +- .../CSP/src/CspMiddlewareExtensions.cs | 18 + .../CSP/src/IContentSecurityPolicyProvider.cs | 11 + .../CSP/{ => src}/LoggingConfiguration.cs | 0 .../{ => src}/Microsoft.AspNetCore.Csp.csproj | 0 .../Microsoft.AspNetCore.Csp.Test.csproj | 11 + src/Middleware/Middleware.sln | 20 +- 23 files changed, 81 insertions(+), 2879 deletions(-) delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsResultTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs delete mode 100644 src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs create mode 100644 src/Middleware/CSP/CSP.slnf rename src/Middleware/CSP/{ => src}/ContentSecurityPolicy.cs (100%) rename src/Middleware/CSP/{ => src}/ContentSecurityPolicyBuilder.cs (100%) rename src/Middleware/CSP/{ => src}/CspMiddleware.cs (91%) create mode 100644 src/Middleware/CSP/src/CspMiddlewareExtensions.cs create mode 100644 src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs rename src/Middleware/CSP/{ => src}/LoggingConfiguration.cs (100%) rename src/Middleware/CSP/{ => src}/Microsoft.AspNetCore.Csp.csproj (100%) create mode 100644 src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 91343d8b1321..d4efeb88839d 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -108,6 +108,7 @@ + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 49d28f78f0f2..7030a737aedb 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -19,6 +19,7 @@ + diff --git a/eng/helix/content/RunTests/RunTestsOptions.cs b/eng/helix/content/RunTests/RunTestsOptions.cs index 9e076c970137..d61057cbdbf0 100644 --- a/eng/helix/content/RunTests/RunTestsOptions.cs +++ b/eng/helix/content/RunTests/RunTestsOptions.cs @@ -85,16 +85,16 @@ public static RunTestsOptions Parse(string[] args) return options; } - public string Target { get; set;} - public string SdkVersion { get; set;} - public string RuntimeVersion { get; set;} - public string AspNetRuntime { get; set;} - public string AspNetRef { get; set;} - public string HelixQueue { get; set;} - public string Architecture { get; set;} - public bool Quarantined { get; set;} - public string EfVersion { get; set;} - public string HELIX_WORKITEM_ROOT { get; set;} + public string Target { get; set; } + public string SdkVersion { get; set; } + public string RuntimeVersion { get; set; } + public string AspNetRuntime { get; set; } + public string AspNetRef { get; set; } + public string HelixQueue { get; set; } + public string Architecture { get; set; } + public bool Quarantined { get; set; } + public string EfVersion { get; set; } + public string HELIX_WORKITEM_ROOT { get; set; } public string DotnetRoot { get; set; } public string Path { get; set; } public TimeSpan Timeout { get; set; } diff --git a/src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs b/src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs deleted file mode 100644 index e800e45813bd..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class CorsEndpointConventionBuilderExtensionsTests - { - [Fact] - public void RequireCors_Name_MetadataAdded() - { - // Arrange - var testConventionBuilder = new TestEndpointConventionBuilder(); - - // Act - testConventionBuilder.RequireCors("TestPolicyName"); - - // Assert - var addCorsPolicy = Assert.Single(testConventionBuilder.Conventions); - - var endpointModel = new TestEndpointBuilder(); - addCorsPolicy(endpointModel); - var endpoint = endpointModel.Build(); - - var metadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(metadata); - Assert.Equal("TestPolicyName", metadata.PolicyName); - } - - [Fact] - public void RequireCors_Policy_MetadataAdded() - { - // Arrange - var testConventionBuilder = new TestEndpointConventionBuilder(); - - // Act - testConventionBuilder.RequireCors(builder => builder.AllowAnyOrigin()); - - // Assert - var addCorsPolicy = Assert.Single(testConventionBuilder.Conventions); - - var endpointBuilder = new TestEndpointBuilder(); - addCorsPolicy(endpointBuilder); - var endpoint = endpointBuilder.Build(); - - var metadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(metadata); - Assert.NotNull(metadata.Policy); - Assert.True(metadata.Policy.AllowAnyOrigin); - } - - [Fact] - public void RequireCors_ChainedCall_ReturnedBuilderIsDerivedType() - { - // Arrange - var testConventionBuilder = new TestEndpointConventionBuilder(); - - // Act - var builder = testConventionBuilder.RequireCors("TestPolicyName"); - - // Assert - Assert.True(builder.TestProperty); - } - - private class TestEndpointBuilder : EndpointBuilder - { - public override Endpoint Build() - { - return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName); - } - } - - private class TestEndpointConventionBuilder : IEndpointConventionBuilder - { - public IList> Conventions { get; } = new List>(); - public bool TestProperty { get; } = true; - - public void Add(Action convention) - { - Conventions.Add(convention); - } - } - } -} diff --git a/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs b/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs deleted file mode 100644 index 4b53a43a5be1..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs +++ /dev/null @@ -1,972 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class CorsMiddlewareTests - { - private const string OriginUrl = "http://api.example.com"; - - [Theory] - [InlineData("PuT")] - [InlineData("PUT")] - public async Task CorsRequest_MatchesPolicy_OnCaseInsensitiveAccessControlRequestMethod(string accessControlRequestMethod) - { - // Arrange - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors(builder => - builder.WithOrigins(OriginUrl) - .WithMethods("PUT")); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => services.AddCors()); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Actual request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .SendAsync(accessControlRequestMethod); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Single(response.Headers); - Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); - Assert.Equal(OriginUrl, response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); - } - } - - [Fact] - public async Task CorsRequest_MatchPolicy_SetsResponseHeaders() - { - // Arrange - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors(builder => - builder.WithOrigins(OriginUrl) - .WithMethods("PUT") - .WithHeaders("Header1") - .WithExposedHeaders("AllowedHeader")); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => services.AddCors()); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Actual request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .SendAsync("PUT"); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Equal(2, response.Headers.Count()); - Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); - Assert.Equal(OriginUrl, response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); - Assert.Equal("AllowedHeader", response.Headers.GetValues(CorsConstants.AccessControlExposeHeaders).FirstOrDefault()); - } - } - - [Theory] - [InlineData("OpTions")] - [InlineData("OPTIONS")] - public async Task PreFlight_MatchesPolicy_OnCaseInsensitiveOptionsMethod(string preflightMethod) - { - // Arrange - var policy = new CorsPolicy(); - policy.Origins.Add(OriginUrl); - policy.Methods.Add("PUT"); - - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors("customPolicy"); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => - { - services.AddCors(options => - { - options.AddPolicy("customPolicy", policy); - }); - }); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Preflight request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .SendAsync(preflightMethod); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Single(response.Headers); - Assert.Equal(OriginUrl, response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); - } - } - - [Fact] - public async Task PreFlight_MatchesPolicy_SetsResponseHeaders() - { - // Arrange - var policy = new CorsPolicy(); - policy.Origins.Add(OriginUrl); - policy.Methods.Add("PUT"); - policy.Headers.Add("Header1"); - policy.ExposedHeaders.Add("AllowedHeader"); - - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors("customPolicy"); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => - { - services.AddCors(options => - { - options.AddPolicy("customPolicy", policy); - }); - }); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Preflight request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") - .SendAsync(CorsConstants.PreflightHttpMethod); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Collection( - response.Headers.OrderBy(h => h.Key), - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); - Assert.Equal(new[] { "Header1" }, kvp.Value); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowMethods, kvp.Key); - Assert.Equal(new[] { "PUT" }, kvp.Value); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); - Assert.Equal(new[] { OriginUrl }, kvp.Value); - }); - } - } - - [Fact] - public async Task PreFlight_WithCredentialsAllowed_ReflectsRequestHeaders() - { - // Arrange - var policy = new CorsPolicyBuilder(OriginUrl) - .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials() - .Build(); - - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors("customPolicy"); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => - { - services.AddCors(options => - { - options.AddPolicy("customPolicy", policy); - }); - }); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Preflight request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") - .AddHeader(CorsConstants.AccessControlRequestHeaders, "X-Test1,X-Test2") - .SendAsync(CorsConstants.PreflightHttpMethod); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Collection( - response.Headers.OrderBy(h => h.Key), - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowCredentials, kvp.Key); - Assert.Equal(new[] { "true" }, kvp.Value); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); - Assert.Equal(new[] { "X-Test1,X-Test2" }, kvp.Value); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowMethods, kvp.Key); - Assert.Equal(new[] { "PUT" }, kvp.Value); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); - Assert.Equal(new[] { OriginUrl }, kvp.Value); - }); - } - } - - [Fact] - public async Task PreFlightRequest_DoesNotMatchPolicy_SetsResponseHeadersAndReturnsNoContent() - { - // Arrange - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors(builder => - builder.WithOrigins(OriginUrl) - .WithMethods("PUT") - .WithHeaders("Header1") - .WithExposedHeaders("AllowedHeader")); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => services.AddCors()); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Preflight request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, "http://test.example.com") - .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") - .SendAsync(CorsConstants.PreflightHttpMethod); - - // Assert - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - Assert.Empty(response.Headers); - } - } - - [Fact] - public async Task CorsRequest_DoesNotMatchPolicy_DoesNotSetHeaders() - { - // Arrange - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors(builder => - builder.WithOrigins(OriginUrl) - .WithMethods("PUT") - .WithHeaders("Header1") - .WithExposedHeaders("AllowedHeader")); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => services.AddCors()); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Actual request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, "http://test.example.com") - .SendAsync("PUT"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(response.Headers); - } - } - - [Fact] - public async Task Uses_PolicyProvider_AsFallback() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - policyName: null); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), - Times.Once); - } - - [Fact] - public async Task DoesNotSetHeaders_ForNoPolicy() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - policyName: null); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.Empty(httpContext.Response.Headers); - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), - Times.Once); - } - - [Fact] - public async Task PreFlight_MatchesDefaultPolicy_SetsResponseHeaders() - { - // Arrange - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors(); - app.Run(async context => - { - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => - { - services.AddCors(options => - { - options.AddDefaultPolicy(policyBuilder => - { - policyBuilder - .WithOrigins(OriginUrl) - .WithMethods("PUT") - .WithHeaders("Header1") - .WithExposedHeaders("AllowedHeader") - .Build(); - }); - options.AddPolicy("policy2", policyBuilder => - { - policyBuilder - .WithOrigins("http://test.example.com") - .Build(); - }); - }); - }); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Preflight request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") - .SendAsync(CorsConstants.PreflightHttpMethod); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Collection( - response.Headers.OrderBy(h => h.Key), - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); - Assert.Equal(new[] { "Header1" }, kvp.Value); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowMethods, kvp.Key); - Assert.Equal(new[] { "PUT" }, kvp.Value); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); - Assert.Equal(new[] { OriginUrl }, kvp.Value); - }); - } - } - - [Fact] - public async Task CorsRequest_SetsResponseHeaders() - { - // Arrange - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCors(builder => - builder.WithOrigins(OriginUrl) - .WithMethods("PUT") - .WithHeaders("Header1") - .WithExposedHeaders("AllowedHeader")); - app.Run(async context => - { - context.Response.Headers.Add("Test", "Should-Appear"); - await context.Response.WriteAsync("Cross origin response"); - }); - }) - .ConfigureServices(services => services.AddCors()); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Actual request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .SendAsync("PUT"); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Collection( - response.Headers.OrderBy(o => o.Key), - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); - Assert.Equal(OriginUrl, Assert.Single(kvp.Value)); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlExposeHeaders, kvp.Key); - Assert.Equal("AllowedHeader", Assert.Single(kvp.Value)); - }, - kvp => - { - Assert.Equal("Test", kvp.Key); - Assert.Equal("Should-Appear", Assert.Single(kvp.Value)); - }); - - Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); - } - } - - [Fact] - public async Task CorsRequest_SetsResponseHeader_IfExceptionHandlerClearsResponse() - { - // Arrange - var exceptionSeen = true; - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - // Simulate ExceptionHandler middleware - app.Use(async (context, next) => - { - try - { - await next(); - } - catch (Exception) - { - exceptionSeen = true; - context.Response.Clear(); - context.Response.StatusCode = 500; - } - }); - - app.UseCors(builder => - builder.WithOrigins(OriginUrl) - .WithMethods("PUT") - .WithHeaders("Header1") - .WithExposedHeaders("AllowedHeader")); - - app.Run(context => - { - context.Response.Headers.Add("Test", "Should-Not-Exist"); - throw new Exception("Runtime error"); - }); - }) - .ConfigureServices(services => services.AddCors()); - - using (var server = new TestServer(hostBuilder)) - { - // Act - // Actual request. - var response = await server.CreateRequest("/") - .AddHeader(CorsConstants.Origin, OriginUrl) - .SendAsync("PUT"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.True(exceptionSeen, "We expect exception middleware to have executed"); - - Assert.Collection( - response.Headers.OrderBy(o => o.Key), - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); - Assert.Equal(OriginUrl, Assert.Single(kvp.Value)); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlExposeHeaders, kvp.Key); - Assert.Equal("AllowedHeader", Assert.Single(kvp.Value)); - }); - } - } - - [Fact] - public async Task Invoke_WithCustomPolicyProviderThatReturnsAsynchronously_Works() - { - // Arrange - var corsService = new CorsService(Options.Create(new CorsOptions()), NullLoggerFactory.Instance); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - var policy = new CorsPolicyBuilder() - .WithOrigins(OriginUrl) - .WithHeaders("AllowedHeader") - .Build(); - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(policy, TimeSpan.FromMilliseconds(10)); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "OPTIONS"; - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { OriginUrl }); - httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "PUT" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - var response = httpContext.Response; - Assert.Collection( - response.Headers.OrderBy(o => o.Key), - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); - Assert.Equal("AllowedHeader", Assert.Single(kvp.Value)); - }, - kvp => - { - Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); - Assert.Equal(OriginUrl, Assert.Single(kvp.Value)); - }); - } - - [Fact] - public async Task Invoke_HasEndpointWithNoMetadata_RunsCors() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), "DefaultPolicyName"), - Times.Once); - } - - [Fact] - public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicyName_RunsCorsWithPolicyName() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName")), "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), "MetadataPolicyName"), - Times.Once); - } - - [Fact] - public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ReturnsNoContentForPreflightRequest() - { - // Arrange - var corsService = Mock.Of(); - var policyProvider = Mock.Of(); - var loggerFactory = NullLoggerFactory.Instance; - - var middleware = new CorsMiddleware( - c => { throw new Exception("Should not be called."); }, - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint")); - httpContext.Request.Method = "OPTIONS"; - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" }); - - // Act - await middleware.Invoke(httpContext, policyProvider); - - // Assert - Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode); - } - - [Fact] - public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ExecutesNextMiddleware() - { - // Arrange - var executed = false; - var corsService = Mock.Of(); - var policyProvider = Mock.Of(); - var loggerFactory = NullLoggerFactory.Instance; - - var middleware = new CorsMiddleware( - c => - { - executed = true; - return Task.CompletedTask; - }, - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint")); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" }); - - // Act - await middleware.Invoke(httpContext, policyProvider); - - // Assert - Assert.True(executed); - Mock.Get(policyProvider).Verify(v => v.GetPolicyAsync(It.IsAny(), It.IsAny()), Times.Never()); - Mock.Get(corsService).Verify(v => v.EvaluatePolicy(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Fact] - public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName() - { - // Arrange - var policy = new CorsPolicyBuilder().Build(); - var corsService = Mock.Of(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - policy, - loggerFactory); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName")), "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), "MetadataPolicyName"), - Times.Once); - } - - [Fact] - public async Task Invoke_HasEndpointRequireCorsMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName() - { - // Arrange - var defaultPolicy = new CorsPolicyBuilder().Build(); - var metadataPolicy = new CorsPolicyBuilder().Build(); - var mockCorsService = new Mock(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - mockCorsService.Setup(o => o.EvaluatePolicy(It.IsAny(), It.IsAny())) - .Returns(new CorsResult()) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - mockCorsService.Object, - defaultPolicy, - loggerFactory); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new CorsPolicyMetadata(metadataPolicy)), "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), - Times.Never); - mockCorsService.Verify( - o => o.EvaluatePolicy(It.IsAny(), metadataPolicy), - Times.Once); - } - - [Fact] - public async Task Invoke_HasEndpointWithEnableMetadataWithNoName_RunsCorsWithStaticPolicy() - { - // Arrange - var policy = new CorsPolicyBuilder().Build(); - var mockCorsService = new Mock(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - mockCorsService.Setup(o => o.EvaluatePolicy(It.IsAny(), It.IsAny())) - .Returns(new CorsResult()) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - mockCorsService.Object, - policy, - loggerFactory); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute()), "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), - Times.Never); - mockCorsService.Verify( - o => o.EvaluatePolicy(It.IsAny(), policy), - Times.Once); - } - - [Fact] - public async Task Invoke_HasEndpointWithDisableMetadata_SkipCors() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new DisableCorsAttribute()), "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), - Times.Never); - } - - [Fact] - public async Task Invoke_HasEndpointWithMutlipleMetadata_SkipCorsBecauseOfMetadataOrder() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = new Mock(); - var loggerFactory = NullLoggerFactory.Instance; - mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(null)) - .Verifiable(); - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider.Object); - - // Assert - mockProvider.Verify( - o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), - Times.Never); - } - - [Fact] - public async Task Invoke_InvokeFlagSet() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = Mock.Of(); - var loggerFactory = NullLoggerFactory.Instance; - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint")); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider); - - // Assert - Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked")); - } - - [Fact] - public async Task Invoke_WithoutOrigin_InvokeFlagSet() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = Mock.Of(); - var loggerFactory = NullLoggerFactory.Instance; - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint")); - - // Act - await middleware.Invoke(httpContext, mockProvider); - - // Assert - Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked")); - } - - [Fact] - public async Task Invoke_WithoutEndpoint_InvokeFlagSet() - { - // Arrange - var corsService = Mock.Of(); - var mockProvider = Mock.Of(); - var loggerFactory = NullLoggerFactory.Instance; - - var middleware = new CorsMiddleware( - Mock.Of(), - corsService, - loggerFactory, - "DefaultPolicyName"); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); - - // Act - await middleware.Invoke(httpContext, mockProvider); - - // Assert - Assert.DoesNotContain(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked")); - } - } -} diff --git a/src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs b/src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs deleted file mode 100644 index 360231d38bef..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class CorsOptionsTest - { - [Fact] - public void AddDefaultPolicy_SetsDefaultPolicyName() - { - // Arrange - var corsOptions = new CorsOptions(); - var expectedPolicy = new CorsPolicy(); - - // Act - corsOptions.AddPolicy("policy1", new CorsPolicy()); - corsOptions.AddDefaultPolicy(expectedPolicy); - corsOptions.AddPolicy("policy3", new CorsPolicy()); - - // Assert - var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); - Assert.Same(expectedPolicy, actualPolicy); - } - - [Fact] - public void AddDefaultPolicy_OverridesDefaultPolicyName() - { - // Arrange - var corsOptions = new CorsOptions(); - var expectedPolicy = new CorsPolicy(); - - // Act - corsOptions.AddDefaultPolicy(new CorsPolicy()); - corsOptions.AddDefaultPolicy(expectedPolicy); - - // Assert - var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); - Assert.Same(expectedPolicy, actualPolicy); - } - - [Fact] - public void AddDefaultPolicy_UsingPolicyBuilder_SetsDefaultPolicyName() - { - // Arrange - var corsOptions = new CorsOptions(); - CorsPolicy expectedPolicy = null; - - // Act - corsOptions.AddPolicy("policy1", policyBuilder => - { - policyBuilder.AllowAnyOrigin().Build(); - }); - corsOptions.AddDefaultPolicy(policyBuilder => - { - expectedPolicy = policyBuilder.AllowAnyOrigin().Build(); - }); - corsOptions.AddPolicy("policy3", new CorsPolicy()); - - // Assert - var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); - Assert.Same(expectedPolicy, actualPolicy); - } - } -} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs b/src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs deleted file mode 100644 index 3dd2bcb22a7a..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class CorsPolicyBuilderTests - { - [Fact] - public void Constructor_WithPolicy_AddsTheGivenPolicy() - { - // Arrange - Func isOriginAllowed = origin => true; - var originalPolicy = new CorsPolicy(); - originalPolicy.Origins.Add("http://existing.com"); - originalPolicy.Headers.Add("Existing"); - originalPolicy.Methods.Add("GET"); - originalPolicy.ExposedHeaders.Add("ExistingExposed"); - originalPolicy.SupportsCredentials = true; - originalPolicy.PreflightMaxAge = TimeSpan.FromSeconds(12); - originalPolicy.IsOriginAllowed = isOriginAllowed; - - // Act - var builder = new CorsPolicyBuilder(originalPolicy); - - // Assert - var corsPolicy = builder.Build(); - - Assert.False(corsPolicy.AllowAnyHeader); - Assert.False(corsPolicy.AllowAnyMethod); - Assert.False(corsPolicy.AllowAnyOrigin); - Assert.True(corsPolicy.SupportsCredentials); - Assert.NotSame(originalPolicy.Headers, corsPolicy.Headers); - Assert.Equal(originalPolicy.Headers, corsPolicy.Headers); - Assert.NotSame(originalPolicy.Methods, corsPolicy.Methods); - Assert.Equal(originalPolicy.Methods, corsPolicy.Methods); - Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins); - Assert.Equal(originalPolicy.Origins, corsPolicy.Origins); - Assert.NotSame(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders); - Assert.Equal(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders); - Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); - Assert.Same(originalPolicy.IsOriginAllowed, corsPolicy.IsOriginAllowed); - } - - [Fact] - public void ConstructorWithPolicy_HavingNullPreflightMaxAge_AddsTheGivenPolicy() - { - // Arrange - var originalPolicy = new CorsPolicy(); - originalPolicy.Origins.Add("http://existing.com"); - - // Act - var builder = new CorsPolicyBuilder(originalPolicy); - - // Assert - var corsPolicy = builder.Build(); - - Assert.Null(corsPolicy.PreflightMaxAge); - Assert.False(corsPolicy.AllowAnyHeader); - Assert.False(corsPolicy.AllowAnyMethod); - Assert.False(corsPolicy.AllowAnyOrigin); - Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins); - Assert.Equal(originalPolicy.Origins, corsPolicy.Origins); - Assert.Empty(corsPolicy.Headers); - Assert.Empty(corsPolicy.Methods); - Assert.Empty(corsPolicy.ExposedHeaders); - } - - [Fact] - public void Constructor_WithNoOrigin() - { - // Arrange & Act - var builder = new CorsPolicyBuilder(); - - // Assert - var corsPolicy = builder.Build(); - Assert.False(corsPolicy.AllowAnyHeader); - Assert.False(corsPolicy.AllowAnyMethod); - Assert.False(corsPolicy.AllowAnyOrigin); - Assert.False(corsPolicy.SupportsCredentials); - Assert.Empty(corsPolicy.ExposedHeaders); - Assert.Empty(corsPolicy.Headers); - Assert.Empty(corsPolicy.Methods); - Assert.Empty(corsPolicy.Origins); - Assert.Null(corsPolicy.PreflightMaxAge); - } - - [Theory] - [InlineData("")] - [InlineData("http://example.com,http://example2.com")] - public void Constructor_WithParamsOrigin_InitializesOrigin(string origin) - { - // Arrange - var origins = origin.Split(','); - - // Act - var builder = new CorsPolicyBuilder(origins); - - // Assert - var corsPolicy = builder.Build(); - Assert.False(corsPolicy.AllowAnyHeader); - Assert.False(corsPolicy.AllowAnyMethod); - Assert.False(corsPolicy.AllowAnyOrigin); - Assert.False(corsPolicy.SupportsCredentials); - Assert.Empty(corsPolicy.ExposedHeaders); - Assert.Empty(corsPolicy.Headers); - Assert.Empty(corsPolicy.Methods); - Assert.Equal(origins.ToList(), corsPolicy.Origins); - Assert.Null(corsPolicy.PreflightMaxAge); - } - - [Fact] - public void WithOrigins_AddsOrigins() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.WithOrigins("http://example.com", "http://example2.com"); - - // Assert - var corsPolicy = builder.Build(); - Assert.False(corsPolicy.AllowAnyOrigin); - Assert.Equal(new List() { "http://example.com", "http://example2.com" }, corsPolicy.Origins); - } - - [Fact] - public void WithOrigins_NormalizesOrigins() - { - // Arrange - var builder = new CorsPolicyBuilder("http://www.EXAMPLE.com", "HTTPS://example2.com"); - - // Assert - var corsPolicy = builder.Build(); - Assert.Equal(new List() { "http://www.example.com", "https://example2.com" }, corsPolicy.Origins); - } - - [Fact] - public void WithOrigins_ThrowsIfArgumentNull() - { - // Arrange - var builder = new CorsPolicyBuilder(); - string[] args = null; - - // Act / Assert - Assert.Throws(() => builder.WithOrigins(args)); - } - - [Fact] - public void WithOrigins_ThrowsIfArgumentArrayContainsNull() - { - // Arrange - var builder = new CorsPolicyBuilder(); - string[] args = new string[] { null }; - - // Act / Assert - Assert.Throws(() => builder.WithOrigins(args)); - } - - [Fact] - public void AllowAnyOrigin_AllowsAny() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.AllowAnyOrigin(); - - // Assert - var corsPolicy = builder.Build(); - Assert.True(corsPolicy.AllowAnyOrigin); - Assert.Equal(new List() { "*" }, corsPolicy.Origins); - } - - [Fact] - public void SetIsOriginAllowed_AddsIsOriginAllowed() - { - // Arrange - var builder = new CorsPolicyBuilder(); - Func isOriginAllowed = origin => true; - - // Act - builder.SetIsOriginAllowed(isOriginAllowed); - - // Assert - var corsPolicy = builder.Build(); - Assert.Same(corsPolicy.IsOriginAllowed, isOriginAllowed); - } - - [Fact] - public void SetIsOriginAllowedToAllowWildcardSubdomains_AllowsWildcardSubdomains() - { - // Arrange - var builder = new CorsPolicyBuilder("http://*.example.com"); - - // Act - builder.SetIsOriginAllowedToAllowWildcardSubdomains(); - - // Assert - var corsPolicy = builder.Build(); - Assert.True(corsPolicy.IsOriginAllowed("http://test.example.com")); - } - - [Fact] - public void SetIsOriginAllowedToAllowWildcardSubdomains_DoesNotAllowRootDomain() - { - // Arrange - var builder = new CorsPolicyBuilder("http://*.example.com"); - - // Act - builder.SetIsOriginAllowedToAllowWildcardSubdomains(); - - // Assert - var corsPolicy = builder.Build(); - Assert.False(corsPolicy.IsOriginAllowed("http://example.com")); - } - - [Fact] - public void WithMethods_AddsMethods() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.WithMethods("PUT", "GET"); - - // Assert - var corsPolicy = builder.Build(); - Assert.False(corsPolicy.AllowAnyOrigin); - Assert.Equal(new List() { "PUT", "GET" }, corsPolicy.Methods); - } - - [Fact] - public void AllowAnyMethod_AllowsAny() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.AllowAnyMethod(); - - // Assert - var corsPolicy = builder.Build(); - Assert.True(corsPolicy.AllowAnyMethod); - Assert.Equal(new List() { "*" }, corsPolicy.Methods); - } - - [Fact] - public void WithHeaders_AddsHeaders() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.WithHeaders("example1", "example2"); - - // Assert - var corsPolicy = builder.Build(); - Assert.False(corsPolicy.AllowAnyHeader); - Assert.Equal(new List() { "example1", "example2" }, corsPolicy.Headers); - } - - [Fact] - public void AllowAnyHeaders_AllowsAny() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.AllowAnyHeader(); - - // Assert - var corsPolicy = builder.Build(); - Assert.True(corsPolicy.AllowAnyHeader); - Assert.Equal(new List() { "*" }, corsPolicy.Headers); - } - - [Fact] - public void WithExposedHeaders_AddsExposedHeaders() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.WithExposedHeaders("exposed1", "exposed2"); - - // Assert - var corsPolicy = builder.Build(); - Assert.Equal(new List() { "exposed1", "exposed2" }, corsPolicy.ExposedHeaders); - } - - [Fact] - public void SetPreFlightMaxAge_SetsThePreFlightAge() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.SetPreflightMaxAge(TimeSpan.FromSeconds(12)); - - // Assert - var corsPolicy = builder.Build(); - Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); - } - - [Fact] - public void AllowCredential_SetsSupportsCredentials_ToTrue() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.AllowCredentials(); - - // Assert - var corsPolicy = builder.Build(); - Assert.True(corsPolicy.SupportsCredentials); - } - - [Fact] - public void DisallowCredential_SetsSupportsCredentials_ToFalse() - { - // Arrange - var builder = new CorsPolicyBuilder(); - - // Act - builder.DisallowCredentials(); - - // Assert - var corsPolicy = builder.Build(); - Assert.False(corsPolicy.SupportsCredentials); - } - - [Fact] - public void Build_ThrowsIfConfiguredToAllowAnyOriginWithCredentials() - { - // Arrange - var builder = new CorsPolicyBuilder() - .AllowAnyOrigin() - .AllowCredentials(); - - // Act - var ex = Assert.Throws(() => builder.Build()); - - // Assert - Assert.Equal(Resources.InsecureConfiguration, ex.Message); - } - - [Theory] - [InlineData("Some-String", "some-string")] - [InlineData("x:\\Test", "x:\\test")] - [InlineData("FTP://Some-url", "ftp://some-url")] - public void GetNormalizedOrigin_ReturnsLowerCasedValue_IfStringIsNotHttpOrHttpsUrl(string origin, string expected) - { - // Act - var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); - - // Assert - Assert.Equal(expected, normalizedOrigin); - } - - [Fact] - public void GetNormalizedOrigin_DoesNotAddPort_IfUriDoesNotSpecifyOne() - { - // Arrange - var origin = "http://www.example.com"; - - // Act - var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); - - // Assert - Assert.Equal(origin, normalizedOrigin); - } - - [Fact] - public void GetNormalizedOrigin_LowerCasesScheme() - { - // Arrange - var origin = "HTTP://www.example.com"; - - // Act - var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); - - // Assert - Assert.Equal("http://www.example.com", normalizedOrigin); - } - - [Fact] - public void GetNormalizedOrigin_LowerCasesHost() - { - // Arrange - var origin = "http://www.Example.Com"; - - // Act - var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); - - // Assert - Assert.Equal("http://www.example.com", normalizedOrigin); - } - - [Theory] - [InlineData("http://www.Example.com:80", "http://www.example.com:80")] - [InlineData("https://www.Example.com:8080", "https://www.example.com:8080")] - public void GetNormalizedOrigin_PreservesPort_ForNonIdnHosts(string origin, string expected) - { - // Act - var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); - - // Assert - Assert.Equal(expected, normalizedOrigin); - } - - [Theory] - [InlineData("http://Bücher.example", "http://xn--bcher-kva.example")] - [InlineData("http://Bücher.example.com:83", "http://xn--bcher-kva.example.com:83")] - [InlineData("https://example.қаз", "https://example.xn--80ao21a")] - // Note that in following case, the default port (443 for HTTPS) is not preserved. - [InlineData("https://www.example.இந்தியா:443", "https://www.example.xn--xkc2dl3a5ee0h")] - public void GetNormalizedOrigin_ReturnsPunyCodedOrigin(string origin, string expected) - { - // Act - var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); - - // Assert - Assert.Equal(expected, normalizedOrigin); - } - } -} diff --git a/src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs b/src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs deleted file mode 100644 index 74dd67db0b07..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public sealed class CorsPolicyExtensionsTest - { - [Fact] - public void IsOriginAnAllowedSubdomain_ReturnsTrueIfPolicyContainsOrigin() - { - // Arrange - const string origin = "http://sub.domain"; - var policy = new CorsPolicy(); - policy.Origins.Add(origin); - - // Act - var actual = policy.IsOriginAnAllowedSubdomain(origin); - - // Assert - Assert.True(actual); - } - - [Theory] - [InlineData(null)] - [InlineData("null")] - [InlineData("http://")] - [InlineData("http://*")] - [InlineData("http://.domain")] - [InlineData("http://.domain/hello")] - public void IsOriginAnAllowedSubdomain_ReturnsFalseIfOriginIsMalformedUri(string malformedOrigin) - { - // Arrange - var policy = new CorsPolicy(); - policy.Origins.Add("http://*.domain"); - - // Act - var actual = policy.IsOriginAnAllowedSubdomain(malformedOrigin); - - // Assert - Assert.False(actual); - } - - [Theory] - [InlineData("http://sub.domain", "http://*.domain")] - [InlineData("http://sub.sub.domain", "http://*.domain")] - [InlineData("http://sub.sub.domain", "http://*.sub.domain")] - [InlineData("http://sub.domain:4567", "http://*.domain:4567")] - public void IsOriginAnAllowedSubdomain_ReturnsTrue_WhenASubdomain(string origin, string allowedOrigin) - { - // Arrange - var policy = new CorsPolicy(); - policy.Origins.Add(allowedOrigin); - - // Act - var isAllowed = policy.IsOriginAnAllowedSubdomain(origin); - - // Assert - Assert.True(isAllowed); - } - - [Theory] - [InlineData("http://domain", "http://*.domain")] - [InlineData("http://sub.domain", "http://domain")] - [InlineData("http://sub.domain:1234", "http://*.domain:5678")] - [InlineData("http://sub.domain", "http://domain.*")] - [InlineData("http://sub.sub.domain", "http://sub.*.domain")] - [InlineData("http://sub.domain.hacker", "http://*.domain")] - [InlineData("https://sub.domain", "http://*.domain")] - public void IsOriginAnAllowedSubdomain_ReturnsFalse_WhenNotASubdomain(string origin, string allowedOrigin) - { - // Arrange - var policy = new CorsPolicy(); - policy.Origins.Add(allowedOrigin); - - // Act - var isAllowed = policy.IsOriginAnAllowedSubdomain(origin); - - // Assert - Assert.False(isAllowed); - } - } -} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs b/src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs deleted file mode 100644 index e99b04096c8e..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class CorsPolicyTest - { - [Fact] - public void Default_Constructor() - { - // Arrange & Act - var corsPolicy = new CorsPolicy(); - - // Assert - Assert.False(corsPolicy.AllowAnyHeader); - Assert.False(corsPolicy.AllowAnyMethod); - Assert.False(corsPolicy.AllowAnyOrigin); - Assert.False(corsPolicy.SupportsCredentials); - Assert.Empty(corsPolicy.ExposedHeaders); - Assert.Empty(corsPolicy.Headers); - Assert.Empty(corsPolicy.Methods); - Assert.Empty(corsPolicy.Origins); - Assert.Null(corsPolicy.PreflightMaxAge); - Assert.NotNull(corsPolicy.IsOriginAllowed); - } - - [Fact] - public void SettingNegativePreflightMaxAge_Throws() - { - // Arrange - var policy = new CorsPolicy(); - - // Act - var exception = Assert.Throws(() => - { - policy.PreflightMaxAge = TimeSpan.FromSeconds(-12); - }); - - // Assert - Assert.Equal( - $"PreflightMaxAge must be greater than or equal to 0. (Parameter 'value')", - exception.Message); - } - - [Fact] - public void ToString_ReturnsThePropertyValues() - { - // Arrange - var corsPolicy = new CorsPolicy - { - PreflightMaxAge = TimeSpan.FromSeconds(12), - SupportsCredentials = true - }; - corsPolicy.Headers.Add("foo"); - corsPolicy.Headers.Add("bar"); - corsPolicy.Origins.Add("http://example.com"); - corsPolicy.Origins.Add("http://example.org"); - corsPolicy.Methods.Add("GET"); - - // Act - var policyString = corsPolicy.ToString(); - - // Assert - Assert.Equal( - @"AllowAnyHeader: False, AllowAnyMethod: False, AllowAnyOrigin: False, PreflightMaxAge: 12,"+ - " SupportsCredentials: True, Origins: {http://example.com,http://example.org}, Methods: {GET},"+ - " Headers: {foo,bar}, ExposedHeaders: {}", - policyString); - } - } -} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsResultTests.cs b/src/Middleware/CORS/test/UnitTests/CorsResultTests.cs deleted file mode 100644 index e537545674f8..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsResultTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class CorsResultTest - { - [Fact] - public void Default_Constructor() - { - // Arrange & Act - var result = new CorsResult(); - - // Assert - Assert.Empty(result.AllowedHeaders); - Assert.Empty(result.AllowedExposedHeaders); - Assert.Empty(result.AllowedMethods); - Assert.False(result.SupportsCredentials); - Assert.Null(result.AllowedOrigin); - Assert.Null(result.PreflightMaxAge); - } - - [Fact] - public void SettingNegativePreflightMaxAge_Throws() - { - // Arrange - var result = new CorsResult(); - - // Act - var exception = Assert.Throws(() => - { - result.PreflightMaxAge = TimeSpan.FromSeconds(-1); - }); - - // Assert - Assert.Equal( - $"PreflightMaxAge must be greater than or equal to 0. (Parameter 'value')", - exception.Message); - } - - [Fact] - public void ToString_ReturnsThePropertyValues() - { - // Arrange - var corsResult = new CorsResult - { - SupportsCredentials = true, - PreflightMaxAge = TimeSpan.FromSeconds(30), - AllowedOrigin = "*" - }; - corsResult.AllowedExposedHeaders.Add("foo"); - corsResult.AllowedHeaders.Add("bar"); - corsResult.AllowedHeaders.Add("baz"); - corsResult.AllowedMethods.Add("GET"); - - // Act - var result = corsResult.ToString(); - - // Assert - Assert.Equal( - @"AllowCredentials: True, PreflightMaxAge: 30, AllowOrigin: *," + - " AllowExposedHeaders: {foo}, AllowHeaders: {bar,baz}, AllowMethods: {GET}", - result); - } - } -} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs b/src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs deleted file mode 100644 index e9bf95531365..000000000000 --- a/src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs +++ /dev/null @@ -1,961 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class CorsServiceTests - { - [Fact] - public void EvaluatePolicy_Throws_IfPolicyIsIncorrectlyConfigured() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext("POST", origin: null); - var policy = new CorsPolicy - { - Origins = { "*" }, - SupportsCredentials = true, - }; - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => corsService.EvaluatePolicy(requestContext, policy), - "policy", - Resources.InsecureConfiguration); - } - - [Fact] - public void EvaluatePolicy_NoOrigin_ReturnsInvalidResult() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext("GET", origin: null); - - // Act - var result = corsService.EvaluatePolicy(requestContext, new CorsPolicy()); - - // Assert - Assert.Null(result.AllowedOrigin); - Assert.False(result.VaryByOrigin); - } - - [Fact] - public void EvaluatePolicy_NoMatchingOrigin_ReturnsInvalidResult() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add("bar"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.False(result.IsOriginAllowed); - } - - [Fact] - public void EvaluatePolicy_EmptyOriginsPolicy_ReturnsInvalidResult() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.False(result.IsOriginAllowed); - } - - [Fact] - public void EvaluatePolicy_IsOriginAllowedReturnsFalse_ReturnsInvalidResult() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy() - { - IsOriginAllowed = origin => false - }; - policy.Origins.Add("example.com"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.False(result.IsOriginAllowed); - } - - [Fact] - public void EvaluatePolicy_AllowAnyOrigin_DoesNotSupportCredentials_EmitsOriginHeader() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - - var policy = new CorsPolicy - { - SupportsCredentials = false - }; - - policy.Origins.Add(CorsConstants.AnyOrigin); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal("*", result.AllowedOrigin); - } - - [Fact] - public void EvaluatePolicy_AllowAnyOrigin_AddsAnyOrigin() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal("*", result.AllowedOrigin); - } - - [Fact] - public void EvaluatePolicy_DoesNotSupportCredentials_AllowCredentialsReturnsFalse() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy - { - SupportsCredentials = false - }; - policy.Origins.Add(CorsConstants.AnyOrigin); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.False(result.SupportsCredentials); - } - - [Fact] - public void EvaluatePolicy_SupportsCredentials_AllowCredentialsReturnsTrue() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy - { - SupportsCredentials = true - }; - policy.Origins.Add("http://example.com"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.True(result.SupportsCredentials); - } - - [Fact] - public void EvaluatePolicy_AllowAnyOrigin_DoesNotSupportsCredentials_DoesNotVaryByOrigin() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal("*", result.AllowedOrigin); - Assert.False(result.VaryByOrigin); - } - - [Fact] - public void EvaluatePolicy_AllowOneOrigin_DoesNotVaryByOrigin() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add("http://example.com"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal("http://example.com", result.AllowedOrigin); - Assert.False(result.VaryByOrigin); - } - - [Fact] - public void EvaluatePolicy_AllowMultipleOrigins_VariesByOrigin() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add("http://example.com"); - policy.Origins.Add("http://api.example.com"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal("http://example.com", result.AllowedOrigin); - Assert.True(result.VaryByOrigin); - } - - [Fact] - public void EvaluatePolicy_NoExposedHeaders_NoAllowExposedHeaders() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Empty(result.AllowedExposedHeaders); - } - - [Fact] - public void EvaluatePolicy_OneExposedHeaders_HeadersAllowed() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.ExposedHeaders.Add("foo"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(new[] { "foo" }, result.AllowedExposedHeaders); - } - - [Fact] - public void EvaluatePolicy_ManyExposedHeaders_HeadersAllowed() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.ExposedHeaders.Add("foo"); - policy.ExposedHeaders.Add("bar"); - policy.ExposedHeaders.Add("baz"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(new[] { "foo", "bar", "baz" }, result.AllowedExposedHeaders); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_MethodNotAllowed() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("GET"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(new[] { "GET" }, result.AllowedMethods); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_MethodAllowed_ReturnsAllowMethods() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("PUT"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.NotNull(result); - Assert.Equal(new[] { "PUT" }, result.AllowedMethods); - } - - [Theory] - [InlineData("OpTions")] - [InlineData("OPTIONS")] - public void EvaluatePolicy_CaseInsensitivePreflightRequest_OriginAllowed_ReturnsOrigin(string preflightMethod) - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext( - method: preflightMethod, - origin: "http://example.com", - accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Origins.Add("http://example.com"); - policy.Methods.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal("http://example.com", result.AllowedOrigin); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_IsOriginAllowedReturnsTrue_ReturnsOrigin() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext( - method: "OPTIONS", - origin: "http://example.com", - accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy - { - IsOriginAllowed = origin => true - }; - policy.Methods.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal("http://example.com", result.AllowedOrigin); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_SupportsCredentials_AllowCredentialsReturnsTrue() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy - { - SupportsCredentials = true - }; - policy.Origins.Add("http://example.com"); - policy.Methods.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.True(result.SupportsCredentials); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_NoPreflightMaxAge_NoPreflightMaxAgeSet() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy - { - PreflightMaxAge = null - }; - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Null(result.PreflightMaxAge); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_PreflightMaxAge_PreflightMaxAgeSet() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy - { - PreflightMaxAge = TimeSpan.FromSeconds(10) - }; - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(TimeSpan.FromSeconds(10), result.PreflightMaxAge); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_AnyMethod_ReturnsRequestMethod() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "GET"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(new[] { "GET" }, result.AllowedMethods); - } - - [Theory] - [InlineData("Put")] - [InlineData("PUT")] - public void EvaluatePolicy_CaseInsensitivePreflightRequest_ReturnsAllowedMethods(string method) - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext( - method: "OPTIONS", - origin: "http://example.com", - accessControlRequestMethod: method); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("PUT"); - policy.Methods.Add("DELETE"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(new[] { "PUT", "DELETE" }, result.AllowedMethods); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_NoHeadersRequested_AllowedAllHeaders() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("*"); - policy.Headers.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Empty(result.AllowedHeaders); - Assert.Equal(new[] { "PUT" }, result.AllowedMethods); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_AllowAllHeaders_ReflectsRequestHeaders() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext( - method: "OPTIONS", - origin: "http://example.com", - accessControlRequestMethod: "PUT", - accessControlRequestHeaders: new[] { "foo", "bar" }); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("*"); - policy.Headers.Add("*"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(new[] { "foo", "bar" }, result.AllowedHeaders); - Assert.Equal(new[] { "PUT" }, result.AllowedMethods); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_HeadersRequested_NotAllHeaderMatches_ReturnsInvalidResult() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext( - method: "OPTIONS", - origin: "http://example.com", - accessControlRequestMethod: "PUT", - accessControlRequestHeaders: new[] { "match", "noMatch" }); - var policy = new CorsPolicy(); - policy.Origins.Add(CorsConstants.AnyOrigin); - policy.Methods.Add("*"); - policy.Headers.Add("match"); - policy.Headers.Add("foo"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.Equal(new[] { "match", "foo" }, result.AllowedHeaders); - Assert.Equal(new[] { "PUT" }, result.AllowedMethods); - } - - [Fact] - public void EvaluatePolicy_PreflightRequest_WithCredentials_ReflectsHeaders() - { - // Arrange - var corsService = GetCorsService(); - var httpContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); - var policy = new CorsPolicy(); - policy.Origins.Add("http://example.com"); - policy.Methods.Add("*"); - policy.Headers.Add("*"); - policy.SupportsCredentials = true; - - // Act - var result = corsService.EvaluatePolicy(httpContext, policy); - - // Assert - Assert.NotNull(result); - Assert.Equal(new[] { "PUT" }, result.AllowedMethods); - Assert.Empty(result.AllowedHeaders); - Assert.True(result.SupportsCredentials); - } - - [Fact] - public void ApplyResult_ReturnsNoHeaders_ByDefault() - { - // Arrange - var result = new CorsResult(); - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Empty(httpContext.Response.Headers); - } - - [Fact] - public void ApplyResult_AllowOrigin_AllowOriginHeaderAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - AllowedOrigin = "http://example.com" - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("http://example.com", httpContext.Response.Headers["Access-Control-Allow-Origin"]); - } - - [Fact] - public void ApplyResult_NoAllowOrigin_AllowOriginHeaderNotAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - AllowedOrigin = null - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.DoesNotContain("Access-Control-Allow-Origin", httpContext.Response.Headers.Keys); - } - - [Fact] - public void ApplyResult_AllowCredentials_AllowCredentialsHeaderAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - SupportsCredentials = true - }; - - var service = GetCorsService(); - - // Act - var httpContext = new DefaultHttpContext(); - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("true", httpContext.Response.Headers["Access-Control-Allow-Credentials"]); - } - - [Fact] - public void ApplyResult_AddVaryHeader_VaryHeaderAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - VaryByOrigin = true - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("Origin", httpContext.Response.Headers["Vary"]); - } - - [Fact] - public void ApplyResult_AppendsVaryHeader() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - VaryByOrigin = true - }; - - var httpContext = new DefaultHttpContext(); - httpContext.Response.Headers["Vary"] = "Cookie"; - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("Cookie,Origin", httpContext.Response.Headers["Vary"]); - } - - [Fact] - public void ApplyResult_NoAllowCredentials_AllowCredentialsHeaderNotAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - SupportsCredentials = false - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.DoesNotContain("Access-Control-Allow-Credentials", httpContext.Response.Headers.Keys); - } - - [Fact] - public void ApplyResult_NoAllowMethods_AllowMethodsHeaderNotAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - // AllowMethods is empty by default - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); - } - - [Fact] - public void ApplyResult_OneAllowMethods_AllowMethodsHeaderAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - IsPreflightRequest = true, - AllowedMethods = { "PUT" } - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("PUT", httpContext.Response.Headers["Access-Control-Allow-Methods"]); - } - - [Fact] - public void ApplyResult_NoAllowHeaders_AllowHeadersHeaderNotAdded() - { - // Arrange - var result = new CorsResult - { - // AllowHeaders is empty by default - IsOriginAllowed = true, - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); - } - - [Fact] - public void ApplyResult_OneAllowHeaders_AllowHeadersHeaderAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - IsPreflightRequest = true, - AllowedHeaders = { "foo" } - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Allow-Headers"]); - } - - - [Fact] - public void ApplyResult_NoAllowExposedHeaders_ExposedHeadersHeaderNotAdded() - { - // Arrange - var result = new CorsResult - { - // AllowExposedHeaders is empty by default - IsOriginAllowed = true, - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.DoesNotContain("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); - } - - [Fact] - public void ApplyResult_PreflightRequest_ExposesHeadersNotAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - IsPreflightRequest = true, - AllowedExposedHeaders = { "foo", "bar" }, - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.DoesNotContain("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); - } - - [Fact] - public void ApplyResult_NoPreflightRequest_ExposesHeadersAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - IsPreflightRequest = false, - AllowedExposedHeaders = { "foo", "bar" }, - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("foo,bar", httpContext.Response.Headers[CorsConstants.AccessControlExposeHeaders]); - } - - - [Fact] - public void ApplyResult_OneAllowExposedHeaders_ExposedHeadersHeaderAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - AllowedExposedHeaders = { "foo" }, - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Expose-Headers"]); - } - - [Fact] - public void ApplyResult_NoPreflightMaxAge_MaxAgeHeaderNotAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - IsPreflightRequest = false, - PreflightMaxAge = TimeSpan.FromSeconds(30), - }; - - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.DoesNotContain("Access-Control-Max-Age", httpContext.Response.Headers.Keys); - } - - [Fact] - public void ApplyResult_PreflightMaxAge_MaxAgeHeaderAdded() - { - // Arrange - var result = new CorsResult - { - IsOriginAllowed = true, - IsPreflightRequest = true, - PreflightMaxAge = TimeSpan.FromSeconds(30), - }; - var httpContext = new DefaultHttpContext(); - var service = GetCorsService(); - - // Act - service.ApplyResult(result, httpContext.Response); - - // Assert - Assert.Equal("30", httpContext.Response.Headers["Access-Control-Max-Age"]); - } - - [Fact] - public void EvaluatePolicy_MultiOriginsPolicy_ReturnsVaryByOriginHeader() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add("http://example.com"); - policy.Origins.Add("http://example-two.com"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.NotNull(result.AllowedOrigin); - Assert.True(result.VaryByOrigin); - } - - [Fact] - public void EvaluatePolicy_MultiOriginsPolicy_NoMatchingOrigin_ReturnsInvalidResult() - { - // Arrange - var corsService = GetCorsService(); - var requestContext = GetHttpContext(origin: "http://example.com"); - var policy = new CorsPolicy(); - policy.Origins.Add("http://example-two.com"); - policy.Origins.Add("http://example-three.com"); - - // Act - var result = corsService.EvaluatePolicy(requestContext, policy); - - // Assert - Assert.False(result.IsOriginAllowed); - } - - private static CorsService GetCorsService(CorsOptions options = null) - { - options = options ?? new CorsOptions(); - return new CorsService(Options.Create(options), NullLoggerFactory.Instance); - } - - private static HttpContext GetHttpContext( - string method = null, - string origin = null, - string accessControlRequestMethod = null, - string[] accessControlRequestHeaders = null) - { - var context = new DefaultHttpContext(); - - if (method != null) - { - context.Request.Method = method; - } - - if (origin != null) - { - context.Request.Headers.Add(CorsConstants.Origin, new[] { origin }); - } - - if (accessControlRequestMethod != null) - { - context.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { accessControlRequestMethod }); - } - - if (accessControlRequestHeaders != null) - { - context.Request.Headers.Add(CorsConstants.AccessControlRequestHeaders, accessControlRequestHeaders); - } - - return context; - } - } -} diff --git a/src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs b/src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs deleted file mode 100644 index e3764bc84ef3..000000000000 --- a/src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public class DefaultPolicyProviderTests - { - [Fact] - public async Task UsesTheDefaultPolicyName() - { - // Arrange - var options = new CorsOptions(); - var policy = new CorsPolicy(); - options.AddPolicy(options.DefaultPolicyName, policy); - - var corsOptions = Options.Create(options); - var policyProvider = new DefaultCorsPolicyProvider(corsOptions); - - // Act - var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName: null); - - // Assert - Assert.Same(policy, actualPolicy); - } - - [Theory] - [InlineData("")] - [InlineData("policyName")] - public async Task GetsNamedPolicy(string policyName) - { - // Arrange - var options = new CorsOptions(); - var policy = new CorsPolicy(); - options.AddPolicy(policyName, policy); - - var corsOptions = Options.Create(options); - var policyProvider = new DefaultCorsPolicyProvider(corsOptions); - - // Act - var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName); - - // Assert - Assert.Same(policy, actualPolicy); - } - } -} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs b/src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs deleted file mode 100644 index 7b28d6aa64e4..000000000000 --- a/src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Xunit; - -namespace Microsoft.AspNetCore.Cors.Infrastructure -{ - public sealed class UriHelpersTests - { - [Theory] - [MemberData(nameof(IsSubdomainOfTestData))] - public void TestIsSubdomainOf(Uri subdomain, Uri domain) - { - // Act - var isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain); - - // Assert - Assert.True(isSubdomain); - } - - [Theory] - [MemberData(nameof(IsNotSubdomainOfTestData))] - public void TestIsSubdomainOf_ReturnsFalse_WhenNotSubdomain(Uri subdomain, Uri domain) - { - // Act - var isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain); - - // Assert - Assert.False(isSubdomain); - } - - public static IEnumerable IsSubdomainOfTestData - { - get - { - return new[] - { - new object[] {new Uri("http://sub.domain"), new Uri("http://domain")}, - new object[] {new Uri("https://sub.domain"), new Uri("https://domain")}, - new object[] {new Uri("https://sub.domain:5678"), new Uri("https://domain:5678")}, - new object[] {new Uri("http://sub.sub.domain"), new Uri("http://domain")}, - new object[] {new Uri("http://sub.sub.domain"), new Uri("http://sub.domain")} - }; - } - } - - public static IEnumerable IsNotSubdomainOfTestData - { - get - { - return new[] - { - new object[] {new Uri("http://subdomain"), new Uri("http://domain")}, - new object[] {new Uri("https://sub.domain"), new Uri("http://domain")}, - new object[] {new Uri("https://sub.domain:1234"), new Uri("https://domain:5678")}, - new object[] {new Uri("http://domain.tld"), new Uri("http://domain")}, - new object[] {new Uri("http://sub.domain.tld"), new Uri("http://domain")}, - new object[] {new Uri("/relativeUri", UriKind.Relative), new Uri("http://domain")}, - new object[] {new Uri("http://sub.domain"), new Uri("/relative", UriKind.Relative)} - }; - } - } - } -} \ No newline at end of file diff --git a/src/Middleware/CSP/CSP.slnf b/src/Middleware/CSP/CSP.slnf new file mode 100644 index 000000000000..1e9c6837c714 --- /dev/null +++ b/src/Middleware/CSP/CSP.slnf @@ -0,0 +1,9 @@ +{ + "solution": { + "path": "D:\\work\\aspnetcore\\src\\Middleware\\Middleware.sln", + "projects": [ + "CSP\\src\\Microsoft.AspNetCore.Csp.csproj", + "CSP\\test\\UnitTests\\Microsoft.AspNetCore.Csp.Test.csproj" + ] + } +} \ No newline at end of file diff --git a/src/Middleware/CSP/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs similarity index 100% rename from src/Middleware/CSP/ContentSecurityPolicy.cs rename to src/Middleware/CSP/src/ContentSecurityPolicy.cs diff --git a/src/Middleware/CSP/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs similarity index 100% rename from src/Middleware/CSP/ContentSecurityPolicyBuilder.cs rename to src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs diff --git a/src/Middleware/CSP/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs similarity index 91% rename from src/Middleware/CSP/CspMiddleware.cs rename to src/Middleware/CSP/src/CspMiddleware.cs index f6a52e4176bc..77d29677e906 100644 --- a/src/Middleware/CSP/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -10,7 +10,7 @@ public class CspMiddleware public Task Invoke(HttpContext context, IContentSecurityPolicyProvider cspProvider) { - + return null; } } } diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs new file mode 100644 index 000000000000..fd3b45b62e52 --- /dev/null +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Csp +{ + public static class CspMiddlewareExtensions + { + public static IApplicationBuilder UseCsp(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} diff --git a/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs b/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs new file mode 100644 index 000000000000..8f4bf6223481 --- /dev/null +++ b/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Csp +{ + public interface IContentSecurityPolicyProvider + { + + Task GetPolicyAsync(HttpContext context); + } +} diff --git a/src/Middleware/CSP/LoggingConfiguration.cs b/src/Middleware/CSP/src/LoggingConfiguration.cs similarity index 100% rename from src/Middleware/CSP/LoggingConfiguration.cs rename to src/Middleware/CSP/src/LoggingConfiguration.cs diff --git a/src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj b/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj similarity index 100% rename from src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj rename to src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj diff --git a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj new file mode 100644 index 000000000000..d17e5e38f70e --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj @@ -0,0 +1,11 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index d441580e1261..5a03da61d2de 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -303,7 +303,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Respon EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSP", "CSP", "{75DC8384-0171-47AC-8510-7502E3892A73}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Csp", "CSP\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp", "CSP\src\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{28323539-12E3-4BCF-91BC-A5AF296679C8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp.Test", "CSP\test\UnitTests\Microsoft.AspNetCore.Csp.Test.csproj", "{F7835CA0-F494-4DE1-AF66-1CB804BC8792}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1659,6 +1663,18 @@ Global {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x64.Build.0 = Release|Any CPU {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.ActiveCfg = Release|Any CPU {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.Build.0 = Release|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x64.Build.0 = Debug|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x86.Build.0 = Debug|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|Any CPU.Build.0 = Release|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x64.ActiveCfg = Release|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x64.Build.0 = Release|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x86.ActiveCfg = Release|Any CPU + {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1789,6 +1805,8 @@ Global {8A9C1F6C-3A47-4868-AA95-3EBE0260F5A0} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472} {80C8E810-1206-482E-BE17-961DD2EBFB11} = {4623F52E-2070-4631-8DEE-7D2F48733FFD} {73A2989B-5F4F-4095-AC25-C705173FA825} = {75DC8384-0171-47AC-8510-7502E3892A73} + {28323539-12E3-4BCF-91BC-A5AF296679C8} = {75DC8384-0171-47AC-8510-7502E3892A73} + {F7835CA0-F494-4DE1-AF66-1CB804BC8792} = {28323539-12E3-4BCF-91BC-A5AF296679C8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA} From a001197b70f1fa08488dfc8340fca22fcc0a785a Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 09:13:13 +0000 Subject: [PATCH 03/26] Revert "Add test project. Fix project structure. Tests can be built and run after this commit." This reverts commit 591cb47dcbcfe80880084a46e8d2365383e80551. --- eng/ProjectReferences.props | 1 - eng/SharedFramework.Local.props | 1 - eng/helix/content/RunTests/RunTestsOptions.cs | 20 +- ...ndpointConventionBuilderExtensionsTests.cs | 90 ++ .../test/UnitTests/CorsMiddlewareTests.cs | 972 ++++++++++++++++++ .../CORS/test/UnitTests/CorsOptionsTest.cs | 67 ++ .../test/UnitTests/CorsPolicyBuilderTests.cs | 432 ++++++++ .../UnitTests/CorsPolicyExtensionsTests.cs | 85 ++ .../CORS/test/UnitTests/CorsPolicyTests.cs | 74 ++ .../CORS/test/UnitTests/CorsResultTests.cs | 69 ++ .../CORS/test/UnitTests/CorsServiceTests.cs | 961 +++++++++++++++++ .../DefaultCorsPolicyProviderTests.cs | 51 + .../CORS/test/UnitTests/UriHelpersTests.cs | 66 ++ src/Middleware/CSP/CSP.slnf | 9 - .../CSP/{src => }/ContentSecurityPolicy.cs | 0 .../{src => }/ContentSecurityPolicyBuilder.cs | 0 src/Middleware/CSP/{src => }/CspMiddleware.cs | 2 +- .../CSP/{src => }/LoggingConfiguration.cs | 0 .../{src => }/Microsoft.AspNetCore.Csp.csproj | 0 .../CSP/src/CspMiddlewareExtensions.cs | 18 - .../CSP/src/IContentSecurityPolicyProvider.cs | 11 - .../Microsoft.AspNetCore.Csp.Test.csproj | 11 - src/Middleware/Middleware.sln | 20 +- 23 files changed, 2879 insertions(+), 81 deletions(-) create mode 100644 src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs create mode 100644 src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/CorsResultTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs create mode 100644 src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs delete mode 100644 src/Middleware/CSP/CSP.slnf rename src/Middleware/CSP/{src => }/ContentSecurityPolicy.cs (100%) rename src/Middleware/CSP/{src => }/ContentSecurityPolicyBuilder.cs (100%) rename src/Middleware/CSP/{src => }/CspMiddleware.cs (91%) rename src/Middleware/CSP/{src => }/LoggingConfiguration.cs (100%) rename src/Middleware/CSP/{src => }/Microsoft.AspNetCore.Csp.csproj (100%) delete mode 100644 src/Middleware/CSP/src/CspMiddlewareExtensions.cs delete mode 100644 src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs delete mode 100644 src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index d4efeb88839d..91343d8b1321 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -108,7 +108,6 @@ - diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 7030a737aedb..49d28f78f0f2 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -19,7 +19,6 @@ - diff --git a/eng/helix/content/RunTests/RunTestsOptions.cs b/eng/helix/content/RunTests/RunTestsOptions.cs index d61057cbdbf0..9e076c970137 100644 --- a/eng/helix/content/RunTests/RunTestsOptions.cs +++ b/eng/helix/content/RunTests/RunTestsOptions.cs @@ -85,16 +85,16 @@ public static RunTestsOptions Parse(string[] args) return options; } - public string Target { get; set; } - public string SdkVersion { get; set; } - public string RuntimeVersion { get; set; } - public string AspNetRuntime { get; set; } - public string AspNetRef { get; set; } - public string HelixQueue { get; set; } - public string Architecture { get; set; } - public bool Quarantined { get; set; } - public string EfVersion { get; set; } - public string HELIX_WORKITEM_ROOT { get; set; } + public string Target { get; set;} + public string SdkVersion { get; set;} + public string RuntimeVersion { get; set;} + public string AspNetRuntime { get; set;} + public string AspNetRef { get; set;} + public string HelixQueue { get; set;} + public string Architecture { get; set;} + public bool Quarantined { get; set;} + public string EfVersion { get; set;} + public string HELIX_WORKITEM_ROOT { get; set;} public string DotnetRoot { get; set; } public string Path { get; set; } public TimeSpan Timeout { get; set; } diff --git a/src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs b/src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs new file mode 100644 index 000000000000..e800e45813bd --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsEndpointConventionBuilderExtensionsTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsEndpointConventionBuilderExtensionsTests + { + [Fact] + public void RequireCors_Name_MetadataAdded() + { + // Arrange + var testConventionBuilder = new TestEndpointConventionBuilder(); + + // Act + testConventionBuilder.RequireCors("TestPolicyName"); + + // Assert + var addCorsPolicy = Assert.Single(testConventionBuilder.Conventions); + + var endpointModel = new TestEndpointBuilder(); + addCorsPolicy(endpointModel); + var endpoint = endpointModel.Build(); + + var metadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(metadata); + Assert.Equal("TestPolicyName", metadata.PolicyName); + } + + [Fact] + public void RequireCors_Policy_MetadataAdded() + { + // Arrange + var testConventionBuilder = new TestEndpointConventionBuilder(); + + // Act + testConventionBuilder.RequireCors(builder => builder.AllowAnyOrigin()); + + // Assert + var addCorsPolicy = Assert.Single(testConventionBuilder.Conventions); + + var endpointBuilder = new TestEndpointBuilder(); + addCorsPolicy(endpointBuilder); + var endpoint = endpointBuilder.Build(); + + var metadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(metadata); + Assert.NotNull(metadata.Policy); + Assert.True(metadata.Policy.AllowAnyOrigin); + } + + [Fact] + public void RequireCors_ChainedCall_ReturnedBuilderIsDerivedType() + { + // Arrange + var testConventionBuilder = new TestEndpointConventionBuilder(); + + // Act + var builder = testConventionBuilder.RequireCors("TestPolicyName"); + + // Assert + Assert.True(builder.TestProperty); + } + + private class TestEndpointBuilder : EndpointBuilder + { + public override Endpoint Build() + { + return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName); + } + } + + private class TestEndpointConventionBuilder : IEndpointConventionBuilder + { + public IList> Conventions { get; } = new List>(); + public bool TestProperty { get; } = true; + + public void Add(Action convention) + { + Conventions.Add(convention); + } + } + } +} diff --git a/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs b/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs new file mode 100644 index 000000000000..4b53a43a5be1 --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs @@ -0,0 +1,972 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsMiddlewareTests + { + private const string OriginUrl = "http://api.example.com"; + + [Theory] + [InlineData("PuT")] + [InlineData("PUT")] + public async Task CorsRequest_MatchesPolicy_OnCaseInsensitiveAccessControlRequestMethod(string accessControlRequestMethod) + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins(OriginUrl) + .WithMethods("PUT")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .SendAsync(accessControlRequestMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Single(response.Headers); + Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); + Assert.Equal(OriginUrl, response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + } + } + + [Fact] + public async Task CorsRequest_MatchPolicy_SetsResponseHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins(OriginUrl) + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .SendAsync("PUT"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(2, response.Headers.Count()); + Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); + Assert.Equal(OriginUrl, response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + Assert.Equal("AllowedHeader", response.Headers.GetValues(CorsConstants.AccessControlExposeHeaders).FirstOrDefault()); + } + } + + [Theory] + [InlineData("OpTions")] + [InlineData("OPTIONS")] + public async Task PreFlight_MatchesPolicy_OnCaseInsensitiveOptionsMethod(string preflightMethod) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add(OriginUrl); + policy.Methods.Add("PUT"); + + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors("customPolicy"); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => + { + services.AddCors(options => + { + options.AddPolicy("customPolicy", policy); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .SendAsync(preflightMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Single(response.Headers); + Assert.Equal(OriginUrl, response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + } + } + + [Fact] + public async Task PreFlight_MatchesPolicy_SetsResponseHeaders() + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add(OriginUrl); + policy.Methods.Add("PUT"); + policy.Headers.Add("Header1"); + policy.ExposedHeaders.Add("AllowedHeader"); + + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors("customPolicy"); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => + { + services.AddCors(options => + { + options.AddPolicy("customPolicy", policy); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Collection( + response.Headers.OrderBy(h => h.Key), + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); + Assert.Equal(new[] { "Header1" }, kvp.Value); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowMethods, kvp.Key); + Assert.Equal(new[] { "PUT" }, kvp.Value); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); + Assert.Equal(new[] { OriginUrl }, kvp.Value); + }); + } + } + + [Fact] + public async Task PreFlight_WithCredentialsAllowed_ReflectsRequestHeaders() + { + // Arrange + var policy = new CorsPolicyBuilder(OriginUrl) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials() + .Build(); + + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors("customPolicy"); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => + { + services.AddCors(options => + { + options.AddPolicy("customPolicy", policy); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .AddHeader(CorsConstants.AccessControlRequestHeaders, "X-Test1,X-Test2") + .SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Collection( + response.Headers.OrderBy(h => h.Key), + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowCredentials, kvp.Key); + Assert.Equal(new[] { "true" }, kvp.Value); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); + Assert.Equal(new[] { "X-Test1,X-Test2" }, kvp.Value); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowMethods, kvp.Key); + Assert.Equal(new[] { "PUT" }, kvp.Value); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); + Assert.Equal(new[] { OriginUrl }, kvp.Value); + }); + } + } + + [Fact] + public async Task PreFlightRequest_DoesNotMatchPolicy_SetsResponseHeadersAndReturnsNoContent() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins(OriginUrl) + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://test.example.com") + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Empty(response.Headers); + } + } + + [Fact] + public async Task CorsRequest_DoesNotMatchPolicy_DoesNotSetHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins(OriginUrl) + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://test.example.com") + .SendAsync("PUT"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + } + } + + [Fact] + public async Task Uses_PolicyProvider_AsFallback() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + policyName: null); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task DoesNotSetHeaders_ForNoPolicy() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + policyName: null); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Empty(httpContext.Response.Headers); + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task PreFlight_MatchesDefaultPolicy_SetsResponseHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => + { + services.AddCors(options => + { + options.AddDefaultPolicy(policyBuilder => + { + policyBuilder + .WithOrigins(OriginUrl) + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader") + .Build(); + }); + options.AddPolicy("policy2", policyBuilder => + { + policyBuilder + .WithOrigins("http://test.example.com") + .Build(); + }); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Collection( + response.Headers.OrderBy(h => h.Key), + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); + Assert.Equal(new[] { "Header1" }, kvp.Value); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowMethods, kvp.Key); + Assert.Equal(new[] { "PUT" }, kvp.Value); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); + Assert.Equal(new[] { OriginUrl }, kvp.Value); + }); + } + } + + [Fact] + public async Task CorsRequest_SetsResponseHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins(OriginUrl) + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + app.Run(async context => + { + context.Response.Headers.Add("Test", "Should-Appear"); + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .SendAsync("PUT"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Collection( + response.Headers.OrderBy(o => o.Key), + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); + Assert.Equal(OriginUrl, Assert.Single(kvp.Value)); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlExposeHeaders, kvp.Key); + Assert.Equal("AllowedHeader", Assert.Single(kvp.Value)); + }, + kvp => + { + Assert.Equal("Test", kvp.Key); + Assert.Equal("Should-Appear", Assert.Single(kvp.Value)); + }); + + Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task CorsRequest_SetsResponseHeader_IfExceptionHandlerClearsResponse() + { + // Arrange + var exceptionSeen = true; + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + // Simulate ExceptionHandler middleware + app.Use(async (context, next) => + { + try + { + await next(); + } + catch (Exception) + { + exceptionSeen = true; + context.Response.Clear(); + context.Response.StatusCode = 500; + } + }); + + app.UseCors(builder => + builder.WithOrigins(OriginUrl) + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + + app.Run(context => + { + context.Response.Headers.Add("Test", "Should-Not-Exist"); + throw new Exception("Runtime error"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, OriginUrl) + .SendAsync("PUT"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.True(exceptionSeen, "We expect exception middleware to have executed"); + + Assert.Collection( + response.Headers.OrderBy(o => o.Key), + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); + Assert.Equal(OriginUrl, Assert.Single(kvp.Value)); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlExposeHeaders, kvp.Key); + Assert.Equal("AllowedHeader", Assert.Single(kvp.Value)); + }); + } + } + + [Fact] + public async Task Invoke_WithCustomPolicyProviderThatReturnsAsynchronously_Works() + { + // Arrange + var corsService = new CorsService(Options.Create(new CorsOptions()), NullLoggerFactory.Instance); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + var policy = new CorsPolicyBuilder() + .WithOrigins(OriginUrl) + .WithHeaders("AllowedHeader") + .Build(); + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(policy, TimeSpan.FromMilliseconds(10)); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "OPTIONS"; + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { OriginUrl }); + httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "PUT" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + var response = httpContext.Response; + Assert.Collection( + response.Headers.OrderBy(o => o.Key), + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowHeaders, kvp.Key); + Assert.Equal("AllowedHeader", Assert.Single(kvp.Value)); + }, + kvp => + { + Assert.Equal(CorsConstants.AccessControlAllowOrigin, kvp.Key); + Assert.Equal(OriginUrl, Assert.Single(kvp.Value)); + }); + } + + [Fact] + public async Task Invoke_HasEndpointWithNoMetadata_RunsCors() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), "DefaultPolicyName"), + Times.Once); + } + + [Fact] + public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicyName_RunsCorsWithPolicyName() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName")), "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), "MetadataPolicyName"), + Times.Once); + } + + [Fact] + public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ReturnsNoContentForPreflightRequest() + { + // Arrange + var corsService = Mock.Of(); + var policyProvider = Mock.Of(); + var loggerFactory = NullLoggerFactory.Instance; + + var middleware = new CorsMiddleware( + c => { throw new Exception("Should not be called."); }, + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint")); + httpContext.Request.Method = "OPTIONS"; + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" }); + + // Act + await middleware.Invoke(httpContext, policyProvider); + + // Assert + Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode); + } + + [Fact] + public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ExecutesNextMiddleware() + { + // Arrange + var executed = false; + var corsService = Mock.Of(); + var policyProvider = Mock.Of(); + var loggerFactory = NullLoggerFactory.Instance; + + var middleware = new CorsMiddleware( + c => + { + executed = true; + return Task.CompletedTask; + }, + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint")); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" }); + + // Act + await middleware.Invoke(httpContext, policyProvider); + + // Assert + Assert.True(executed); + Mock.Get(policyProvider).Verify(v => v.GetPolicyAsync(It.IsAny(), It.IsAny()), Times.Never()); + Mock.Get(corsService).Verify(v => v.EvaluatePolicy(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName() + { + // Arrange + var policy = new CorsPolicyBuilder().Build(); + var corsService = Mock.Of(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + policy, + loggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName")), "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), "MetadataPolicyName"), + Times.Once); + } + + [Fact] + public async Task Invoke_HasEndpointRequireCorsMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName() + { + // Arrange + var defaultPolicy = new CorsPolicyBuilder().Build(); + var metadataPolicy = new CorsPolicyBuilder().Build(); + var mockCorsService = new Mock(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + mockCorsService.Setup(o => o.EvaluatePolicy(It.IsAny(), It.IsAny())) + .Returns(new CorsResult()) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + mockCorsService.Object, + defaultPolicy, + loggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new CorsPolicyMetadata(metadataPolicy)), "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Never); + mockCorsService.Verify( + o => o.EvaluatePolicy(It.IsAny(), metadataPolicy), + Times.Once); + } + + [Fact] + public async Task Invoke_HasEndpointWithEnableMetadataWithNoName_RunsCorsWithStaticPolicy() + { + // Arrange + var policy = new CorsPolicyBuilder().Build(); + var mockCorsService = new Mock(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + mockCorsService.Setup(o => o.EvaluatePolicy(It.IsAny(), It.IsAny())) + .Returns(new CorsResult()) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + mockCorsService.Object, + policy, + loggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute()), "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Never); + mockCorsService.Verify( + o => o.EvaluatePolicy(It.IsAny(), policy), + Times.Once); + } + + [Fact] + public async Task Invoke_HasEndpointWithDisableMetadata_SkipCors() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new DisableCorsAttribute()), "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task Invoke_HasEndpointWithMutlipleMetadata_SkipCorsBecauseOfMetadataOrder() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + var loggerFactory = NullLoggerFactory.Instance; + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider.Object); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task Invoke_InvokeFlagSet() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = Mock.Of(); + var loggerFactory = NullLoggerFactory.Instance; + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint")); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider); + + // Assert + Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked")); + } + + [Fact] + public async Task Invoke_WithoutOrigin_InvokeFlagSet() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = Mock.Of(); + var loggerFactory = NullLoggerFactory.Instance; + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint")); + + // Act + await middleware.Invoke(httpContext, mockProvider); + + // Assert + Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked")); + } + + [Fact] + public async Task Invoke_WithoutEndpoint_InvokeFlagSet() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = Mock.Of(); + var loggerFactory = NullLoggerFactory.Instance; + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext, mockProvider); + + // Assert + Assert.DoesNotContain(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked")); + } + } +} diff --git a/src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs b/src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs new file mode 100644 index 000000000000..360231d38bef --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsOptionsTest.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsOptionsTest + { + [Fact] + public void AddDefaultPolicy_SetsDefaultPolicyName() + { + // Arrange + var corsOptions = new CorsOptions(); + var expectedPolicy = new CorsPolicy(); + + // Act + corsOptions.AddPolicy("policy1", new CorsPolicy()); + corsOptions.AddDefaultPolicy(expectedPolicy); + corsOptions.AddPolicy("policy3", new CorsPolicy()); + + // Assert + var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); + Assert.Same(expectedPolicy, actualPolicy); + } + + [Fact] + public void AddDefaultPolicy_OverridesDefaultPolicyName() + { + // Arrange + var corsOptions = new CorsOptions(); + var expectedPolicy = new CorsPolicy(); + + // Act + corsOptions.AddDefaultPolicy(new CorsPolicy()); + corsOptions.AddDefaultPolicy(expectedPolicy); + + // Assert + var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); + Assert.Same(expectedPolicy, actualPolicy); + } + + [Fact] + public void AddDefaultPolicy_UsingPolicyBuilder_SetsDefaultPolicyName() + { + // Arrange + var corsOptions = new CorsOptions(); + CorsPolicy expectedPolicy = null; + + // Act + corsOptions.AddPolicy("policy1", policyBuilder => + { + policyBuilder.AllowAnyOrigin().Build(); + }); + corsOptions.AddDefaultPolicy(policyBuilder => + { + expectedPolicy = policyBuilder.AllowAnyOrigin().Build(); + }); + corsOptions.AddPolicy("policy3", new CorsPolicy()); + + // Assert + var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); + Assert.Same(expectedPolicy, actualPolicy); + } + } +} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs b/src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs new file mode 100644 index 000000000000..3dd2bcb22a7a --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsPolicyBuilderTests.cs @@ -0,0 +1,432 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsPolicyBuilderTests + { + [Fact] + public void Constructor_WithPolicy_AddsTheGivenPolicy() + { + // Arrange + Func isOriginAllowed = origin => true; + var originalPolicy = new CorsPolicy(); + originalPolicy.Origins.Add("http://existing.com"); + originalPolicy.Headers.Add("Existing"); + originalPolicy.Methods.Add("GET"); + originalPolicy.ExposedHeaders.Add("ExistingExposed"); + originalPolicy.SupportsCredentials = true; + originalPolicy.PreflightMaxAge = TimeSpan.FromSeconds(12); + originalPolicy.IsOriginAllowed = isOriginAllowed; + + // Act + var builder = new CorsPolicyBuilder(originalPolicy); + + // Assert + var corsPolicy = builder.Build(); + + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.True(corsPolicy.SupportsCredentials); + Assert.NotSame(originalPolicy.Headers, corsPolicy.Headers); + Assert.Equal(originalPolicy.Headers, corsPolicy.Headers); + Assert.NotSame(originalPolicy.Methods, corsPolicy.Methods); + Assert.Equal(originalPolicy.Methods, corsPolicy.Methods); + Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins); + Assert.Equal(originalPolicy.Origins, corsPolicy.Origins); + Assert.NotSame(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders); + Assert.Equal(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders); + Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); + Assert.Same(originalPolicy.IsOriginAllowed, corsPolicy.IsOriginAllowed); + } + + [Fact] + public void ConstructorWithPolicy_HavingNullPreflightMaxAge_AddsTheGivenPolicy() + { + // Arrange + var originalPolicy = new CorsPolicy(); + originalPolicy.Origins.Add("http://existing.com"); + + // Act + var builder = new CorsPolicyBuilder(originalPolicy); + + // Assert + var corsPolicy = builder.Build(); + + Assert.Null(corsPolicy.PreflightMaxAge); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins); + Assert.Equal(originalPolicy.Origins, corsPolicy.Origins); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.ExposedHeaders); + } + + [Fact] + public void Constructor_WithNoOrigin() + { + // Arrange & Act + var builder = new CorsPolicyBuilder(); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + } + + [Theory] + [InlineData("")] + [InlineData("http://example.com,http://example2.com")] + public void Constructor_WithParamsOrigin_InitializesOrigin(string origin) + { + // Arrange + var origins = origin.Split(','); + + // Act + var builder = new CorsPolicyBuilder(origins); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Equal(origins.ToList(), corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + } + + [Fact] + public void WithOrigins_AddsOrigins() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithOrigins("http://example.com", "http://example2.com"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "http://example.com", "http://example2.com" }, corsPolicy.Origins); + } + + [Fact] + public void WithOrigins_NormalizesOrigins() + { + // Arrange + var builder = new CorsPolicyBuilder("http://www.EXAMPLE.com", "HTTPS://example2.com"); + + // Assert + var corsPolicy = builder.Build(); + Assert.Equal(new List() { "http://www.example.com", "https://example2.com" }, corsPolicy.Origins); + } + + [Fact] + public void WithOrigins_ThrowsIfArgumentNull() + { + // Arrange + var builder = new CorsPolicyBuilder(); + string[] args = null; + + // Act / Assert + Assert.Throws(() => builder.WithOrigins(args)); + } + + [Fact] + public void WithOrigins_ThrowsIfArgumentArrayContainsNull() + { + // Arrange + var builder = new CorsPolicyBuilder(); + string[] args = new string[] { null }; + + // Act / Assert + Assert.Throws(() => builder.WithOrigins(args)); + } + + [Fact] + public void AllowAnyOrigin_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyOrigin(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "*" }, corsPolicy.Origins); + } + + [Fact] + public void SetIsOriginAllowed_AddsIsOriginAllowed() + { + // Arrange + var builder = new CorsPolicyBuilder(); + Func isOriginAllowed = origin => true; + + // Act + builder.SetIsOriginAllowed(isOriginAllowed); + + // Assert + var corsPolicy = builder.Build(); + Assert.Same(corsPolicy.IsOriginAllowed, isOriginAllowed); + } + + [Fact] + public void SetIsOriginAllowedToAllowWildcardSubdomains_AllowsWildcardSubdomains() + { + // Arrange + var builder = new CorsPolicyBuilder("http://*.example.com"); + + // Act + builder.SetIsOriginAllowedToAllowWildcardSubdomains(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.IsOriginAllowed("http://test.example.com")); + } + + [Fact] + public void SetIsOriginAllowedToAllowWildcardSubdomains_DoesNotAllowRootDomain() + { + // Arrange + var builder = new CorsPolicyBuilder("http://*.example.com"); + + // Act + builder.SetIsOriginAllowedToAllowWildcardSubdomains(); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.IsOriginAllowed("http://example.com")); + } + + [Fact] + public void WithMethods_AddsMethods() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithMethods("PUT", "GET"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "PUT", "GET" }, corsPolicy.Methods); + } + + [Fact] + public void AllowAnyMethod_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyMethod(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyMethod); + Assert.Equal(new List() { "*" }, corsPolicy.Methods); + } + + [Fact] + public void WithHeaders_AddsHeaders() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithHeaders("example1", "example2"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.Equal(new List() { "example1", "example2" }, corsPolicy.Headers); + } + + [Fact] + public void AllowAnyHeaders_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyHeader(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyHeader); + Assert.Equal(new List() { "*" }, corsPolicy.Headers); + } + + [Fact] + public void WithExposedHeaders_AddsExposedHeaders() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithExposedHeaders("exposed1", "exposed2"); + + // Assert + var corsPolicy = builder.Build(); + Assert.Equal(new List() { "exposed1", "exposed2" }, corsPolicy.ExposedHeaders); + } + + [Fact] + public void SetPreFlightMaxAge_SetsThePreFlightAge() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.SetPreflightMaxAge(TimeSpan.FromSeconds(12)); + + // Assert + var corsPolicy = builder.Build(); + Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); + } + + [Fact] + public void AllowCredential_SetsSupportsCredentials_ToTrue() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowCredentials(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.SupportsCredentials); + } + + [Fact] + public void DisallowCredential_SetsSupportsCredentials_ToFalse() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.DisallowCredentials(); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.SupportsCredentials); + } + + [Fact] + public void Build_ThrowsIfConfiguredToAllowAnyOriginWithCredentials() + { + // Arrange + var builder = new CorsPolicyBuilder() + .AllowAnyOrigin() + .AllowCredentials(); + + // Act + var ex = Assert.Throws(() => builder.Build()); + + // Assert + Assert.Equal(Resources.InsecureConfiguration, ex.Message); + } + + [Theory] + [InlineData("Some-String", "some-string")] + [InlineData("x:\\Test", "x:\\test")] + [InlineData("FTP://Some-url", "ftp://some-url")] + public void GetNormalizedOrigin_ReturnsLowerCasedValue_IfStringIsNotHttpOrHttpsUrl(string origin, string expected) + { + // Act + var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); + + // Assert + Assert.Equal(expected, normalizedOrigin); + } + + [Fact] + public void GetNormalizedOrigin_DoesNotAddPort_IfUriDoesNotSpecifyOne() + { + // Arrange + var origin = "http://www.example.com"; + + // Act + var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); + + // Assert + Assert.Equal(origin, normalizedOrigin); + } + + [Fact] + public void GetNormalizedOrigin_LowerCasesScheme() + { + // Arrange + var origin = "HTTP://www.example.com"; + + // Act + var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); + + // Assert + Assert.Equal("http://www.example.com", normalizedOrigin); + } + + [Fact] + public void GetNormalizedOrigin_LowerCasesHost() + { + // Arrange + var origin = "http://www.Example.Com"; + + // Act + var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); + + // Assert + Assert.Equal("http://www.example.com", normalizedOrigin); + } + + [Theory] + [InlineData("http://www.Example.com:80", "http://www.example.com:80")] + [InlineData("https://www.Example.com:8080", "https://www.example.com:8080")] + public void GetNormalizedOrigin_PreservesPort_ForNonIdnHosts(string origin, string expected) + { + // Act + var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); + + // Assert + Assert.Equal(expected, normalizedOrigin); + } + + [Theory] + [InlineData("http://Bücher.example", "http://xn--bcher-kva.example")] + [InlineData("http://Bücher.example.com:83", "http://xn--bcher-kva.example.com:83")] + [InlineData("https://example.қаз", "https://example.xn--80ao21a")] + // Note that in following case, the default port (443 for HTTPS) is not preserved. + [InlineData("https://www.example.இந்தியா:443", "https://www.example.xn--xkc2dl3a5ee0h")] + public void GetNormalizedOrigin_ReturnsPunyCodedOrigin(string origin, string expected) + { + // Act + var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin); + + // Assert + Assert.Equal(expected, normalizedOrigin); + } + } +} diff --git a/src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs b/src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs new file mode 100644 index 000000000000..74dd67db0b07 --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsPolicyExtensionsTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public sealed class CorsPolicyExtensionsTest + { + [Fact] + public void IsOriginAnAllowedSubdomain_ReturnsTrueIfPolicyContainsOrigin() + { + // Arrange + const string origin = "http://sub.domain"; + var policy = new CorsPolicy(); + policy.Origins.Add(origin); + + // Act + var actual = policy.IsOriginAnAllowedSubdomain(origin); + + // Assert + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("null")] + [InlineData("http://")] + [InlineData("http://*")] + [InlineData("http://.domain")] + [InlineData("http://.domain/hello")] + public void IsOriginAnAllowedSubdomain_ReturnsFalseIfOriginIsMalformedUri(string malformedOrigin) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add("http://*.domain"); + + // Act + var actual = policy.IsOriginAnAllowedSubdomain(malformedOrigin); + + // Assert + Assert.False(actual); + } + + [Theory] + [InlineData("http://sub.domain", "http://*.domain")] + [InlineData("http://sub.sub.domain", "http://*.domain")] + [InlineData("http://sub.sub.domain", "http://*.sub.domain")] + [InlineData("http://sub.domain:4567", "http://*.domain:4567")] + public void IsOriginAnAllowedSubdomain_ReturnsTrue_WhenASubdomain(string origin, string allowedOrigin) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add(allowedOrigin); + + // Act + var isAllowed = policy.IsOriginAnAllowedSubdomain(origin); + + // Assert + Assert.True(isAllowed); + } + + [Theory] + [InlineData("http://domain", "http://*.domain")] + [InlineData("http://sub.domain", "http://domain")] + [InlineData("http://sub.domain:1234", "http://*.domain:5678")] + [InlineData("http://sub.domain", "http://domain.*")] + [InlineData("http://sub.sub.domain", "http://sub.*.domain")] + [InlineData("http://sub.domain.hacker", "http://*.domain")] + [InlineData("https://sub.domain", "http://*.domain")] + public void IsOriginAnAllowedSubdomain_ReturnsFalse_WhenNotASubdomain(string origin, string allowedOrigin) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add(allowedOrigin); + + // Act + var isAllowed = policy.IsOriginAnAllowedSubdomain(origin); + + // Assert + Assert.False(isAllowed); + } + } +} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs b/src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs new file mode 100644 index 000000000000..e99b04096c8e --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsPolicyTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsPolicyTest + { + [Fact] + public void Default_Constructor() + { + // Arrange & Act + var corsPolicy = new CorsPolicy(); + + // Assert + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + Assert.NotNull(corsPolicy.IsOriginAllowed); + } + + [Fact] + public void SettingNegativePreflightMaxAge_Throws() + { + // Arrange + var policy = new CorsPolicy(); + + // Act + var exception = Assert.Throws(() => + { + policy.PreflightMaxAge = TimeSpan.FromSeconds(-12); + }); + + // Assert + Assert.Equal( + $"PreflightMaxAge must be greater than or equal to 0. (Parameter 'value')", + exception.Message); + } + + [Fact] + public void ToString_ReturnsThePropertyValues() + { + // Arrange + var corsPolicy = new CorsPolicy + { + PreflightMaxAge = TimeSpan.FromSeconds(12), + SupportsCredentials = true + }; + corsPolicy.Headers.Add("foo"); + corsPolicy.Headers.Add("bar"); + corsPolicy.Origins.Add("http://example.com"); + corsPolicy.Origins.Add("http://example.org"); + corsPolicy.Methods.Add("GET"); + + // Act + var policyString = corsPolicy.ToString(); + + // Assert + Assert.Equal( + @"AllowAnyHeader: False, AllowAnyMethod: False, AllowAnyOrigin: False, PreflightMaxAge: 12,"+ + " SupportsCredentials: True, Origins: {http://example.com,http://example.org}, Methods: {GET},"+ + " Headers: {foo,bar}, ExposedHeaders: {}", + policyString); + } + } +} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsResultTests.cs b/src/Middleware/CORS/test/UnitTests/CorsResultTests.cs new file mode 100644 index 000000000000..e537545674f8 --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsResultTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsResultTest + { + [Fact] + public void Default_Constructor() + { + // Arrange & Act + var result = new CorsResult(); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.False(result.SupportsCredentials); + Assert.Null(result.AllowedOrigin); + Assert.Null(result.PreflightMaxAge); + } + + [Fact] + public void SettingNegativePreflightMaxAge_Throws() + { + // Arrange + var result = new CorsResult(); + + // Act + var exception = Assert.Throws(() => + { + result.PreflightMaxAge = TimeSpan.FromSeconds(-1); + }); + + // Assert + Assert.Equal( + $"PreflightMaxAge must be greater than or equal to 0. (Parameter 'value')", + exception.Message); + } + + [Fact] + public void ToString_ReturnsThePropertyValues() + { + // Arrange + var corsResult = new CorsResult + { + SupportsCredentials = true, + PreflightMaxAge = TimeSpan.FromSeconds(30), + AllowedOrigin = "*" + }; + corsResult.AllowedExposedHeaders.Add("foo"); + corsResult.AllowedHeaders.Add("bar"); + corsResult.AllowedHeaders.Add("baz"); + corsResult.AllowedMethods.Add("GET"); + + // Act + var result = corsResult.ToString(); + + // Assert + Assert.Equal( + @"AllowCredentials: True, PreflightMaxAge: 30, AllowOrigin: *," + + " AllowExposedHeaders: {foo}, AllowHeaders: {bar,baz}, AllowMethods: {GET}", + result); + } + } +} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs b/src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs new file mode 100644 index 000000000000..e9bf95531365 --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/CorsServiceTests.cs @@ -0,0 +1,961 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsServiceTests + { + [Fact] + public void EvaluatePolicy_Throws_IfPolicyIsIncorrectlyConfigured() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext("POST", origin: null); + var policy = new CorsPolicy + { + Origins = { "*" }, + SupportsCredentials = true, + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => corsService.EvaluatePolicy(requestContext, policy), + "policy", + Resources.InsecureConfiguration); + } + + [Fact] + public void EvaluatePolicy_NoOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext("GET", origin: null); + + // Act + var result = corsService.EvaluatePolicy(requestContext, new CorsPolicy()); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_NoMatchingOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("bar"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.False(result.IsOriginAllowed); + } + + [Fact] + public void EvaluatePolicy_EmptyOriginsPolicy_ReturnsInvalidResult() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.False(result.IsOriginAllowed); + } + + [Fact] + public void EvaluatePolicy_IsOriginAllowedReturnsFalse_ReturnsInvalidResult() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy() + { + IsOriginAllowed = origin => false + }; + policy.Origins.Add("example.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.False(result.IsOriginAllowed); + } + + [Fact] + public void EvaluatePolicy_AllowAnyOrigin_DoesNotSupportCredentials_EmitsOriginHeader() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + + var policy = new CorsPolicy + { + SupportsCredentials = false + }; + + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("*", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_AllowAnyOrigin_AddsAnyOrigin() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("*", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_DoesNotSupportCredentials_AllowCredentialsReturnsFalse() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = false + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.False(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_SupportsCredentials_AllowCredentialsReturnsTrue() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add("http://example.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.True(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_AllowAnyOrigin_DoesNotSupportsCredentials_DoesNotVaryByOrigin() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("*", result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_AllowOneOrigin_DoesNotVaryByOrigin() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://example.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_AllowMultipleOrigins_VariesByOrigin() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://example.com"); + policy.Origins.Add("http://api.example.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + Assert.True(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_NoExposedHeaders_NoAllowExposedHeaders() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_OneExposedHeaders_HeadersAllowed() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.ExposedHeaders.Add("foo"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(new[] { "foo" }, result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_ManyExposedHeaders_HeadersAllowed() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.ExposedHeaders.Add("foo"); + policy.ExposedHeaders.Add("bar"); + policy.ExposedHeaders.Add("baz"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(new[] { "foo", "bar", "baz" }, result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_MethodNotAllowed() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("GET"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(new[] { "GET" }, result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_MethodAllowed_ReturnsAllowMethods() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("PUT"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.NotNull(result); + Assert.Equal(new[] { "PUT" }, result.AllowedMethods); + } + + [Theory] + [InlineData("OpTions")] + [InlineData("OPTIONS")] + public void EvaluatePolicy_CaseInsensitivePreflightRequest_OriginAllowed_ReturnsOrigin(string preflightMethod) + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext( + method: preflightMethod, + origin: "http://example.com", + accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Origins.Add("http://example.com"); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_IsOriginAllowedReturnsTrue_ReturnsOrigin() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + IsOriginAllowed = origin => true + }; + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_SupportsCredentials_AllowCredentialsReturnsTrue() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add("http://example.com"); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.True(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_NoPreflightMaxAge_NoPreflightMaxAgeSet() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + PreflightMaxAge = null + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.PreflightMaxAge); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_PreflightMaxAge_PreflightMaxAgeSet() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + PreflightMaxAge = TimeSpan.FromSeconds(10) + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(TimeSpan.FromSeconds(10), result.PreflightMaxAge); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_AnyMethod_ReturnsRequestMethod() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "GET"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(new[] { "GET" }, result.AllowedMethods); + } + + [Theory] + [InlineData("Put")] + [InlineData("PUT")] + public void EvaluatePolicy_CaseInsensitivePreflightRequest_ReturnsAllowedMethods(string method) + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: method); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("PUT"); + policy.Methods.Add("DELETE"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(new[] { "PUT", "DELETE" }, result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_NoHeadersRequested_AllowedAllHeaders() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Equal(new[] { "PUT" }, result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_AllowAllHeaders_ReflectsRequestHeaders() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "foo", "bar" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(new[] { "foo", "bar" }, result.AllowedHeaders); + Assert.Equal(new[] { "PUT" }, result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_HeadersRequested_NotAllHeaderMatches_ReturnsInvalidResult() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "match", "noMatch" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("match"); + policy.Headers.Add("foo"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(new[] { "match", "foo" }, result.AllowedHeaders); + Assert.Equal(new[] { "PUT" }, result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_WithCredentials_ReflectsHeaders() + { + // Arrange + var corsService = GetCorsService(); + var httpContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://example.com"); + policy.Methods.Add("*"); + policy.Headers.Add("*"); + policy.SupportsCredentials = true; + + // Act + var result = corsService.EvaluatePolicy(httpContext, policy); + + // Assert + Assert.NotNull(result); + Assert.Equal(new[] { "PUT" }, result.AllowedMethods); + Assert.Empty(result.AllowedHeaders); + Assert.True(result.SupportsCredentials); + } + + [Fact] + public void ApplyResult_ReturnsNoHeaders_ByDefault() + { + // Arrange + var result = new CorsResult(); + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Empty(httpContext.Response.Headers); + } + + [Fact] + public void ApplyResult_AllowOrigin_AllowOriginHeaderAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + AllowedOrigin = "http://example.com" + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("http://example.com", httpContext.Response.Headers["Access-Control-Allow-Origin"]); + } + + [Fact] + public void ApplyResult_NoAllowOrigin_AllowOriginHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + AllowedOrigin = null + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Origin", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_AllowCredentials_AllowCredentialsHeaderAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + SupportsCredentials = true + }; + + var service = GetCorsService(); + + // Act + var httpContext = new DefaultHttpContext(); + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("true", httpContext.Response.Headers["Access-Control-Allow-Credentials"]); + } + + [Fact] + public void ApplyResult_AddVaryHeader_VaryHeaderAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + VaryByOrigin = true + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("Origin", httpContext.Response.Headers["Vary"]); + } + + [Fact] + public void ApplyResult_AppendsVaryHeader() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + VaryByOrigin = true + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Response.Headers["Vary"] = "Cookie"; + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("Cookie,Origin", httpContext.Response.Headers["Vary"]); + } + + [Fact] + public void ApplyResult_NoAllowCredentials_AllowCredentialsHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + SupportsCredentials = false + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Credentials", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoAllowMethods_AllowMethodsHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + // AllowMethods is empty by default + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowMethods_AllowMethodsHeaderAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + IsPreflightRequest = true, + AllowedMethods = { "PUT" } + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("PUT", httpContext.Response.Headers["Access-Control-Allow-Methods"]); + } + + [Fact] + public void ApplyResult_NoAllowHeaders_AllowHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowHeaders is empty by default + IsOriginAllowed = true, + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowHeaders_AllowHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + IsPreflightRequest = true, + AllowedHeaders = { "foo" } + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Allow-Headers"]); + } + + + [Fact] + public void ApplyResult_NoAllowExposedHeaders_ExposedHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowExposedHeaders is empty by default + IsOriginAllowed = true, + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_PreflightRequest_ExposesHeadersNotAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + IsPreflightRequest = true, + AllowedExposedHeaders = { "foo", "bar" }, + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoPreflightRequest_ExposesHeadersAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + IsPreflightRequest = false, + AllowedExposedHeaders = { "foo", "bar" }, + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("foo,bar", httpContext.Response.Headers[CorsConstants.AccessControlExposeHeaders]); + } + + + [Fact] + public void ApplyResult_OneAllowExposedHeaders_ExposedHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + AllowedExposedHeaders = { "foo" }, + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Expose-Headers"]); + } + + [Fact] + public void ApplyResult_NoPreflightMaxAge_MaxAgeHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + IsPreflightRequest = false, + PreflightMaxAge = TimeSpan.FromSeconds(30), + }; + + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Max-Age", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_PreflightMaxAge_MaxAgeHeaderAdded() + { + // Arrange + var result = new CorsResult + { + IsOriginAllowed = true, + IsPreflightRequest = true, + PreflightMaxAge = TimeSpan.FromSeconds(30), + }; + var httpContext = new DefaultHttpContext(); + var service = GetCorsService(); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("30", httpContext.Response.Headers["Access-Control-Max-Age"]); + } + + [Fact] + public void EvaluatePolicy_MultiOriginsPolicy_ReturnsVaryByOriginHeader() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://example.com"); + policy.Origins.Add("http://example-two.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.NotNull(result.AllowedOrigin); + Assert.True(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_MultiOriginsPolicy_NoMatchingOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = GetCorsService(); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://example-two.com"); + policy.Origins.Add("http://example-three.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.False(result.IsOriginAllowed); + } + + private static CorsService GetCorsService(CorsOptions options = null) + { + options = options ?? new CorsOptions(); + return new CorsService(Options.Create(options), NullLoggerFactory.Instance); + } + + private static HttpContext GetHttpContext( + string method = null, + string origin = null, + string accessControlRequestMethod = null, + string[] accessControlRequestHeaders = null) + { + var context = new DefaultHttpContext(); + + if (method != null) + { + context.Request.Method = method; + } + + if (origin != null) + { + context.Request.Headers.Add(CorsConstants.Origin, new[] { origin }); + } + + if (accessControlRequestMethod != null) + { + context.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { accessControlRequestMethod }); + } + + if (accessControlRequestHeaders != null) + { + context.Request.Headers.Add(CorsConstants.AccessControlRequestHeaders, accessControlRequestHeaders); + } + + return context; + } + } +} diff --git a/src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs b/src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs new file mode 100644 index 000000000000..e3764bc84ef3 --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/DefaultCorsPolicyProviderTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class DefaultPolicyProviderTests + { + [Fact] + public async Task UsesTheDefaultPolicyName() + { + // Arrange + var options = new CorsOptions(); + var policy = new CorsPolicy(); + options.AddPolicy(options.DefaultPolicyName, policy); + + var corsOptions = Options.Create(options); + var policyProvider = new DefaultCorsPolicyProvider(corsOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName: null); + + // Assert + Assert.Same(policy, actualPolicy); + } + + [Theory] + [InlineData("")] + [InlineData("policyName")] + public async Task GetsNamedPolicy(string policyName) + { + // Arrange + var options = new CorsOptions(); + var policy = new CorsPolicy(); + options.AddPolicy(policyName, policy); + + var corsOptions = Options.Create(options); + var policyProvider = new DefaultCorsPolicyProvider(corsOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName); + + // Assert + Assert.Same(policy, actualPolicy); + } + } +} \ No newline at end of file diff --git a/src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs b/src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs new file mode 100644 index 000000000000..7b28d6aa64e4 --- /dev/null +++ b/src/Middleware/CORS/test/UnitTests/UriHelpersTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public sealed class UriHelpersTests + { + [Theory] + [MemberData(nameof(IsSubdomainOfTestData))] + public void TestIsSubdomainOf(Uri subdomain, Uri domain) + { + // Act + var isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain); + + // Assert + Assert.True(isSubdomain); + } + + [Theory] + [MemberData(nameof(IsNotSubdomainOfTestData))] + public void TestIsSubdomainOf_ReturnsFalse_WhenNotSubdomain(Uri subdomain, Uri domain) + { + // Act + var isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain); + + // Assert + Assert.False(isSubdomain); + } + + public static IEnumerable IsSubdomainOfTestData + { + get + { + return new[] + { + new object[] {new Uri("http://sub.domain"), new Uri("http://domain")}, + new object[] {new Uri("https://sub.domain"), new Uri("https://domain")}, + new object[] {new Uri("https://sub.domain:5678"), new Uri("https://domain:5678")}, + new object[] {new Uri("http://sub.sub.domain"), new Uri("http://domain")}, + new object[] {new Uri("http://sub.sub.domain"), new Uri("http://sub.domain")} + }; + } + } + + public static IEnumerable IsNotSubdomainOfTestData + { + get + { + return new[] + { + new object[] {new Uri("http://subdomain"), new Uri("http://domain")}, + new object[] {new Uri("https://sub.domain"), new Uri("http://domain")}, + new object[] {new Uri("https://sub.domain:1234"), new Uri("https://domain:5678")}, + new object[] {new Uri("http://domain.tld"), new Uri("http://domain")}, + new object[] {new Uri("http://sub.domain.tld"), new Uri("http://domain")}, + new object[] {new Uri("/relativeUri", UriKind.Relative), new Uri("http://domain")}, + new object[] {new Uri("http://sub.domain"), new Uri("/relative", UriKind.Relative)} + }; + } + } + } +} \ No newline at end of file diff --git a/src/Middleware/CSP/CSP.slnf b/src/Middleware/CSP/CSP.slnf deleted file mode 100644 index 1e9c6837c714..000000000000 --- a/src/Middleware/CSP/CSP.slnf +++ /dev/null @@ -1,9 +0,0 @@ -{ - "solution": { - "path": "D:\\work\\aspnetcore\\src\\Middleware\\Middleware.sln", - "projects": [ - "CSP\\src\\Microsoft.AspNetCore.Csp.csproj", - "CSP\\test\\UnitTests\\Microsoft.AspNetCore.Csp.Test.csproj" - ] - } -} \ No newline at end of file diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/ContentSecurityPolicy.cs similarity index 100% rename from src/Middleware/CSP/src/ContentSecurityPolicy.cs rename to src/Middleware/CSP/ContentSecurityPolicy.cs diff --git a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/ContentSecurityPolicyBuilder.cs similarity index 100% rename from src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs rename to src/Middleware/CSP/ContentSecurityPolicyBuilder.cs diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/CspMiddleware.cs similarity index 91% rename from src/Middleware/CSP/src/CspMiddleware.cs rename to src/Middleware/CSP/CspMiddleware.cs index 77d29677e906..f6a52e4176bc 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/CspMiddleware.cs @@ -10,7 +10,7 @@ public class CspMiddleware public Task Invoke(HttpContext context, IContentSecurityPolicyProvider cspProvider) { - return null; + } } } diff --git a/src/Middleware/CSP/src/LoggingConfiguration.cs b/src/Middleware/CSP/LoggingConfiguration.cs similarity index 100% rename from src/Middleware/CSP/src/LoggingConfiguration.cs rename to src/Middleware/CSP/LoggingConfiguration.cs diff --git a/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj b/src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj similarity index 100% rename from src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj rename to src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs deleted file mode 100644 index fd3b45b62e52..000000000000 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; - -namespace Microsoft.AspNetCore.Csp -{ - public static class CspMiddlewareExtensions - { - public static IApplicationBuilder UseCsp(this IApplicationBuilder app) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); - } - } -} diff --git a/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs b/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs deleted file mode 100644 index 8f4bf6223481..000000000000 --- a/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Csp -{ - public interface IContentSecurityPolicyProvider - { - - Task GetPolicyAsync(HttpContext context); - } -} diff --git a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj deleted file mode 100644 index d17e5e38f70e..000000000000 --- a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - $(DefaultNetCoreTargetFramework) - - - - - - - diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index 5a03da61d2de..d441580e1261 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -303,11 +303,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Respon EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSP", "CSP", "{75DC8384-0171-47AC-8510-7502E3892A73}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp", "CSP\src\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{28323539-12E3-4BCF-91BC-A5AF296679C8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp.Test", "CSP\test\UnitTests\Microsoft.AspNetCore.Csp.Test.csproj", "{F7835CA0-F494-4DE1-AF66-1CB804BC8792}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Csp", "CSP\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1663,18 +1659,6 @@ Global {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x64.Build.0 = Release|Any CPU {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.ActiveCfg = Release|Any CPU {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.Build.0 = Release|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x64.ActiveCfg = Debug|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x64.Build.0 = Debug|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x86.ActiveCfg = Debug|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Debug|x86.Build.0 = Debug|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|Any CPU.Build.0 = Release|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x64.ActiveCfg = Release|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x64.Build.0 = Release|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x86.ActiveCfg = Release|Any CPU - {F7835CA0-F494-4DE1-AF66-1CB804BC8792}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1805,8 +1789,6 @@ Global {8A9C1F6C-3A47-4868-AA95-3EBE0260F5A0} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472} {80C8E810-1206-482E-BE17-961DD2EBFB11} = {4623F52E-2070-4631-8DEE-7D2F48733FFD} {73A2989B-5F4F-4095-AC25-C705173FA825} = {75DC8384-0171-47AC-8510-7502E3892A73} - {28323539-12E3-4BCF-91BC-A5AF296679C8} = {75DC8384-0171-47AC-8510-7502E3892A73} - {F7835CA0-F494-4DE1-AF66-1CB804BC8792} = {28323539-12E3-4BCF-91BC-A5AF296679C8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA} From cc530ad30444d7cf584d3e6e5489a401f86772d4 Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 09:23:51 +0000 Subject: [PATCH 04/26] Fix project structure without deleting half of .NET Core --- eng/ProjectReferences.props | 1 + eng/SharedFramework.Local.props | 1 + src/Middleware/CSP/CSP.slnf | 9 +++++++++ .../CSP/{ => src}/ContentSecurityPolicy.cs | 0 .../{ => src}/ContentSecurityPolicyBuilder.cs | 0 src/Middleware/CSP/{ => src}/CspMiddleware.cs | 2 +- .../CSP/src/IContentSecurityPolicyProvider.cs | 11 ++++++++++ .../CSP/{ => src}/LoggingConfiguration.cs | 0 .../{ => src}/Microsoft.AspNetCore.Csp.csproj | 0 .../Microsoft.AspNetCore.Csp.Test.csproj | 11 ++++++++++ src/Middleware/Middleware.sln | 20 ++++++++++++++++++- 11 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/Middleware/CSP/CSP.slnf rename src/Middleware/CSP/{ => src}/ContentSecurityPolicy.cs (100%) rename src/Middleware/CSP/{ => src}/ContentSecurityPolicyBuilder.cs (100%) rename src/Middleware/CSP/{ => src}/CspMiddleware.cs (91%) create mode 100644 src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs rename src/Middleware/CSP/{ => src}/LoggingConfiguration.cs (100%) rename src/Middleware/CSP/{ => src}/Microsoft.AspNetCore.Csp.csproj (100%) create mode 100644 src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 91343d8b1321..d4efeb88839d 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -108,6 +108,7 @@ + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 49d28f78f0f2..7030a737aedb 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -19,6 +19,7 @@ + diff --git a/src/Middleware/CSP/CSP.slnf b/src/Middleware/CSP/CSP.slnf new file mode 100644 index 000000000000..723739ea61da --- /dev/null +++ b/src/Middleware/CSP/CSP.slnf @@ -0,0 +1,9 @@ +{ + "solution": { + "path": "D:\\work\\aspnetcore\\src\\Middleware\\Middleware.sln", + "projects": [ + "CSP\\src\\Microsoft.AspNetCore.Csp.csproj", + "CSP\\test\\UnitTests\\Microsoft.AspNetCore.Csp.Test.csproj", + ] + } +} \ No newline at end of file diff --git a/src/Middleware/CSP/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs similarity index 100% rename from src/Middleware/CSP/ContentSecurityPolicy.cs rename to src/Middleware/CSP/src/ContentSecurityPolicy.cs diff --git a/src/Middleware/CSP/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs similarity index 100% rename from src/Middleware/CSP/ContentSecurityPolicyBuilder.cs rename to src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs diff --git a/src/Middleware/CSP/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs similarity index 91% rename from src/Middleware/CSP/CspMiddleware.cs rename to src/Middleware/CSP/src/CspMiddleware.cs index f6a52e4176bc..77d29677e906 100644 --- a/src/Middleware/CSP/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -10,7 +10,7 @@ public class CspMiddleware public Task Invoke(HttpContext context, IContentSecurityPolicyProvider cspProvider) { - + return null; } } } diff --git a/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs b/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs new file mode 100644 index 000000000000..8f4bf6223481 --- /dev/null +++ b/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Csp +{ + public interface IContentSecurityPolicyProvider + { + + Task GetPolicyAsync(HttpContext context); + } +} diff --git a/src/Middleware/CSP/LoggingConfiguration.cs b/src/Middleware/CSP/src/LoggingConfiguration.cs similarity index 100% rename from src/Middleware/CSP/LoggingConfiguration.cs rename to src/Middleware/CSP/src/LoggingConfiguration.cs diff --git a/src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj b/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj similarity index 100% rename from src/Middleware/CSP/Microsoft.AspNetCore.Csp.csproj rename to src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj diff --git a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj new file mode 100644 index 000000000000..c7347d5a2511 --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj @@ -0,0 +1,11 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + \ No newline at end of file diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index d441580e1261..60e41c20623e 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -303,7 +303,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Respon EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSP", "CSP", "{75DC8384-0171-47AC-8510-7502E3892A73}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Csp", "CSP\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp", "CSP\src\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{723A26AE-C8A3-49D4-8CBE-83FEAB46FFAC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp.Test", "CSP\test\UnitTests\Microsoft.AspNetCore.Csp.Test.csproj", "{A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1659,6 +1663,18 @@ Global {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x64.Build.0 = Release|Any CPU {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.ActiveCfg = Release|Any CPU {73A2989B-5F4F-4095-AC25-C705173FA825}.Release|x86.Build.0 = Release|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Debug|x64.Build.0 = Debug|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Debug|x86.Build.0 = Debug|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Release|Any CPU.Build.0 = Release|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Release|x64.ActiveCfg = Release|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Release|x64.Build.0 = Release|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Release|x86.ActiveCfg = Release|Any CPU + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1789,6 +1805,8 @@ Global {8A9C1F6C-3A47-4868-AA95-3EBE0260F5A0} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472} {80C8E810-1206-482E-BE17-961DD2EBFB11} = {4623F52E-2070-4631-8DEE-7D2F48733FFD} {73A2989B-5F4F-4095-AC25-C705173FA825} = {75DC8384-0171-47AC-8510-7502E3892A73} + {723A26AE-C8A3-49D4-8CBE-83FEAB46FFAC} = {75DC8384-0171-47AC-8510-7502E3892A73} + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7} = {723A26AE-C8A3-49D4-8CBE-83FEAB46FFAC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA} From 9456e7cfe5e47c6a7273ecd75f7cd1bd63a2e1cd Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 10:50:12 +0000 Subject: [PATCH 05/26] Add CSP service and first middleware test. Currently failing. --- src/Middleware/CSP/src/CspMiddleware.cs | 9 +++- .../CSP/src/CspMiddlewareExtensions.cs | 18 ++++++++ src/Middleware/CSP/src/CspOptions.cs | 12 ++++++ src/Middleware/CSP/src/CspService.cs | 18 ++++++++ .../CSP/src/CspServiceCollectionExtensions.cs | 43 +++++++++++++++++++ .../DefaultContentSecurityPolicyProvider.cs | 17 ++++++++ src/Middleware/CSP/src/ICspService.cs | 9 ++++ .../CSP/src/Microsoft.AspNetCore.Csp.csproj | 4 ++ .../CSP/test/UnitTests/CspMiddlewareTests.cs | 42 ++++++++++++++++++ 9 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/CSP/src/CspMiddlewareExtensions.cs create mode 100644 src/Middleware/CSP/src/CspOptions.cs create mode 100644 src/Middleware/CSP/src/CspService.cs create mode 100644 src/Middleware/CSP/src/CspServiceCollectionExtensions.cs create mode 100644 src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs create mode 100644 src/Middleware/CSP/src/ICspService.cs create mode 100644 src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 77d29677e906..3dec618d9333 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -6,11 +6,18 @@ namespace Microsoft.AspNetCore.Csp { public class CspMiddleware { + private readonly RequestDelegate _next; + private readonly ICspService _cspService; + public CspMiddleware(RequestDelegate next, ICspService cspService) + { + _next = next; + _cspService = cspService; + } public Task Invoke(HttpContext context, IContentSecurityPolicyProvider cspProvider) { - return null; + return _next(context); } } } diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs new file mode 100644 index 000000000000..fd3b45b62e52 --- /dev/null +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Csp +{ + public static class CspMiddlewareExtensions + { + public static IApplicationBuilder UseCsp(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} diff --git a/src/Middleware/CSP/src/CspOptions.cs b/src/Middleware/CSP/src/CspOptions.cs new file mode 100644 index 000000000000..fdfc1b2dae48 --- /dev/null +++ b/src/Middleware/CSP/src/CspOptions.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Csp +{ + public class CspOptions + { + } +} diff --git a/src/Middleware/CSP/src/CspService.cs b/src/Middleware/CSP/src/CspService.cs new file mode 100644 index 000000000000..7391db6afb34 --- /dev/null +++ b/src/Middleware/CSP/src/CspService.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Csp +{ + public class CspService : ICspService + { + public CspService() + { + + } + + public void ApplyResult(HttpResponse response) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Middleware/CSP/src/CspServiceCollectionExtensions.cs b/src/Middleware/CSP/src/CspServiceCollectionExtensions.cs new file mode 100644 index 000000000000..143512871e9d --- /dev/null +++ b/src/Middleware/CSP/src/CspServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Csp; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CspServiceCollectionExtensions + { + public static IServiceCollection AddCsp(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddOptions(); + + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + + return services; + } + + public static IServiceCollection AddCsp(this IServiceCollection services, Action setupAction) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + services.AddCsp(); + services.Configure(setupAction); + + return services; + } + } +} diff --git a/src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs b/src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs new file mode 100644 index 000000000000..b4fc43e120f7 --- /dev/null +++ b/src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Csp +{ + public class DefaultContentSecurityPolicyProvider : IContentSecurityPolicyProvider + { + public Task GetPolicyAsync(HttpContext context) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Middleware/CSP/src/ICspService.cs b/src/Middleware/CSP/src/ICspService.cs new file mode 100644 index 000000000000..47e5e5d628a0 --- /dev/null +++ b/src/Middleware/CSP/src/ICspService.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Csp +{ + public interface ICspService + { + void ApplyResult(HttpResponse response); + } +} diff --git a/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj b/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj index 1a7ffd8b5504..19c6ddd81aec 100644 --- a/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj +++ b/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj @@ -11,6 +11,10 @@ + + + + diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs new file mode 100644 index 000000000000..41d8932c8ce9 --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -0,0 +1,42 @@ +using Xunit; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Csp.Test +{ + public class CspMiddlewareTests + { + [Fact] + public async Task test() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCsp(); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCsp()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Single(response.Headers); + Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); + } + } + } +} From 3b1e8c1318fcadcf07d14b10b93665925ada6497 Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 11:16:16 +0000 Subject: [PATCH 06/26] First passing end to end test setting static CSP header --- src/Middleware/CSP/src/ContentSecurityPolicy.cs | 6 ++++++ src/Middleware/CSP/src/CspConstants.cs | 13 +++++++++++++ src/Middleware/CSP/src/CspMiddleware.cs | 7 +++++-- src/Middleware/CSP/src/CspMiddlewareExtensions.cs | 7 +++++-- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 13 ++++++++++--- 5 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 src/Middleware/CSP/src/CspConstants.cs diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index 26b9e3839c5e..81ff7b8d0941 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -15,11 +15,17 @@ public enum CspMode public class ContentSecurityPolicy { + //TODO: Remove getters public CspMode CspMode { get; internal set; } public bool StrictDynamic { get; internal set; } public bool UnsafeEval { get; internal set; } public string ReportingUri { get; internal set; } public LoggingConfiguration LoggingConfiguration { get; internal set; } public bool ReportOnly { get; internal set; } + + public string GetPolicy() + { + return "object-src 'none'; script-src 'nonce-{random}' 'strict-dynamic' https: http:; base-uri 'none'; "; + } } } diff --git a/src/Middleware/CSP/src/CspConstants.cs b/src/Middleware/CSP/src/CspConstants.cs new file mode 100644 index 000000000000..bb57229cdcdd --- /dev/null +++ b/src/Middleware/CSP/src/CspConstants.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Csp +{ + public static class CspConstants + { + public static readonly string CspHeaderKey = "Content-Security-Policy"; + } +} diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 3dec618d9333..1c6a183b02b2 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -8,15 +8,18 @@ public class CspMiddleware { private readonly RequestDelegate _next; private readonly ICspService _cspService; + private readonly ContentSecurityPolicy _csp; - public CspMiddleware(RequestDelegate next, ICspService cspService) + public CspMiddleware(RequestDelegate next, ICspService cspService, ContentSecurityPolicy csp) { _next = next; _cspService = cspService; + _csp = csp; } - public Task Invoke(HttpContext context, IContentSecurityPolicyProvider cspProvider) + public Task Invoke(HttpContext context) { + context.Response.Headers[CspConstants.CspHeaderKey] = _csp.GetPolicy(); return _next(context); } } diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index fd3b45b62e52..f98dcde82d4c 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -5,14 +5,17 @@ namespace Microsoft.AspNetCore.Csp { public static class CspMiddlewareExtensions { - public static IApplicationBuilder UseCsp(this IApplicationBuilder app) + public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action configurePolicy) { if (app == null) { throw new ArgumentNullException(nameof(app)); } - return app.UseMiddleware(); + var policyBuilder = new ContentSecurityPolicyBuilder(); + configurePolicy(policyBuilder); + + return app.UseMiddleware(policyBuilder.Build()); } } } diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 41d8932c8ce9..98c9f736527a 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; +using System.Linq; namespace Microsoft.AspNetCore.Csp.Test { @@ -17,10 +18,14 @@ public async Task test() var hostBuilder = new WebHostBuilder() .Configure(app => { - app.UseCsp(); + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING); + }); app.Run(async context => { - await context.Response.WriteAsync("Cross origin response"); + await context.Response.WriteAsync("Test response"); }); }) .ConfigureServices(services => services.AddCsp()); @@ -35,7 +40,9 @@ public async Task test() // Assert response.EnsureSuccessStatusCode(); Assert.Single(response.Headers); - Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); + var expectedPolicy = "object-src 'none'; script-src 'nonce-{random}' 'strict-dynamic' https: http:; base-uri 'none'; "; + Assert.Equal(expectedPolicy, response.Headers.GetValues(CspConstants.CspHeaderKey).FirstOrDefault()); + Assert.Equal("Test response", await response.Content.ReadAsStringAsync()); } } } From f18d6e36f8c3227f768eab7e4cd14f04e6e10d6e Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 12:04:58 +0000 Subject: [PATCH 07/26] Add policy builder and policy tests --- .../CSP/src/ContentSecurityPolicy.cs | 22 ++-- .../CSP/src/ContentSecurityPolicyBuilder.cs | 29 ++--- src/Middleware/CSP/src/CspConstants.cs | 3 +- src/Middleware/CSP/src/CspMiddleware.cs | 2 +- .../CSP/src/LoggingConfiguration.cs | 2 + .../ContentSecurityPolicyBuilderTest.cs | 41 +++++++ .../UnitTests/ContentSecurityPolicyTest.cs | 102 ++++++++++++++++++ .../CSP/test/UnitTests/CspMiddlewareTests.cs | 2 - 8 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs create mode 100644 src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index 81ff7b8d0941..86679e07867d 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -15,17 +15,27 @@ public enum CspMode public class ContentSecurityPolicy { + private readonly string _baseAndObject = "base-uri 'none'; object-src 'none'"; + + public string GetHeaderName() + { + return CspMode == CspMode.REPORTING ? CspConstants.CspReportingHeaderName : CspConstants.CspEnforcedHeaderName; + } + public string GetPolicy() + { + return string.Format( + "script-src 'nonce-random' {0} {1} https: http:; {2}; {3}", + StrictDynamic ? "'strict-dynamic'" : "", + UnsafeEval ? "'unsafe-eval'" : "", + _baseAndObject, + ReportingUri != null ? "report-uri " + ReportingUri : ""); + } + //TODO: Remove getters public CspMode CspMode { get; internal set; } public bool StrictDynamic { get; internal set; } public bool UnsafeEval { get; internal set; } public string ReportingUri { get; internal set; } public LoggingConfiguration LoggingConfiguration { get; internal set; } - public bool ReportOnly { get; internal set; } - - public string GetPolicy() - { - return "object-src 'none'; script-src 'nonce-{random}' 'strict-dynamic' https: http:; base-uri 'none'; "; - } } } diff --git a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs index 38a69845bc57..de2c587de6c4 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs @@ -1,14 +1,18 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.AspNetCore.Csp { public class ContentSecurityPolicyBuilder { private readonly ContentSecurityPolicy _policy = new ContentSecurityPolicy(); + private LoggingConfiguration _LoggingConfiguration; + + public ContentSecurityPolicyBuilder() + { + // TODO: Consider adding builder for logging config + _LoggingConfiguration = new LoggingConfiguration(); + _LoggingConfiguration.LogLevel = Extensions.Logging.LogLevel.Information; + } public ContentSecurityPolicyBuilder WithCspMode(CspMode cspMode) { @@ -27,12 +31,6 @@ public ContentSecurityPolicyBuilder WithUnsafeEval() _policy.UnsafeEval = true; return this; } - - public ContentSecurityPolicyBuilder WithReportOnly() - { - _policy.ReportOnly = true; - return this; - } public ContentSecurityPolicyBuilder WithReportingUri(string reportingUri) { // TODO: normalize URL @@ -42,7 +40,7 @@ public ContentSecurityPolicyBuilder WithReportingUri(string reportingUri) public ContentSecurityPolicyBuilder WithLoggingConfiguration(LoggingConfiguration loggingConfiguration) { - _policy.LoggingConfiguration = loggingConfiguration; + _LoggingConfiguration = loggingConfiguration; return this; } @@ -54,18 +52,13 @@ public ContentSecurityPolicy Build() throw new InvalidOperationException(); } - if (_policy.ReportOnly && _policy.ReportingUri == null) - { - // TODO: Error message - throw new InvalidOperationException(); - } - - if (_policy.ReportOnly && _policy.LoggingConfiguration == null) + if (_policy.CspMode == CspMode.REPORTING && _policy.ReportingUri == null) { // TODO: Error message throw new InvalidOperationException(); } + _policy.LoggingConfiguration = _LoggingConfiguration; return _policy; } } diff --git a/src/Middleware/CSP/src/CspConstants.cs b/src/Middleware/CSP/src/CspConstants.cs index bb57229cdcdd..688c22f8d115 100644 --- a/src/Middleware/CSP/src/CspConstants.cs +++ b/src/Middleware/CSP/src/CspConstants.cs @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Csp { public static class CspConstants { - public static readonly string CspHeaderKey = "Content-Security-Policy"; + public static readonly string CspEnforcedHeaderName = "Content-Security-Policy"; + public static readonly string CspReportingHeaderName = "Content-Security-Policy-Report-Only"; } } diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 1c6a183b02b2..69427a81f5d8 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -19,7 +19,7 @@ public CspMiddleware(RequestDelegate next, ICspService cspService, ContentSecuri public Task Invoke(HttpContext context) { - context.Response.Headers[CspConstants.CspHeaderKey] = _csp.GetPolicy(); + context.Response.Headers[CspConstants.CspEnforcedHeaderName] = _csp.GetPolicy(); return _next(context); } } diff --git a/src/Middleware/CSP/src/LoggingConfiguration.cs b/src/Middleware/CSP/src/LoggingConfiguration.cs index ca7434f680eb..06d5f4591c75 100644 --- a/src/Middleware/CSP/src/LoggingConfiguration.cs +++ b/src/Middleware/CSP/src/LoggingConfiguration.cs @@ -3,10 +3,12 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp { public class LoggingConfiguration { + public LogLevel LogLevel { get; internal set; } } } diff --git a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs new file mode 100644 index 000000000000..c713e451452e --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs @@ -0,0 +1,41 @@ + + +using System; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Csp.Test +{ + public class ContentSecurityPolicyBuilderTest + { + [Fact] + public void IfCspModeNotSet_thenExceptionThrown() + { + Assert.Throws( + () => new ContentSecurityPolicyBuilder() + .Build() + ); + } + + [Fact] + public void WhenNoLoggingConfigurationSet_thenDefaultLoggingConfigurationUsed() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .Build(); + + //TODO: Define default logging config + Assert.Equal(LogLevel.Information, policy.LoggingConfiguration.LogLevel); + } + + [Fact] + public void WhenModeSetToReporting_IfNoReportingUriSet_thenExceptionThrown() + { + Assert.Throws( + () => new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.REPORTING) + .Build() + ); + } + } +} diff --git a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs new file mode 100644 index 000000000000..21beade61768 --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs @@ -0,0 +1,102 @@ +using Xunit; + +namespace Microsoft.AspNetCore.Csp.Test +{ + public class ContentSecurityPolicyTest + { + [Fact] + public void SetsCorrectHeaderNameInReportingMode() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.REPORTING) + .WithReportingUri("/csp") + .Build(); + + Assert.Equal(CspConstants.CspReportingHeaderName, policy.GetHeaderName()); + } + + [Fact] + public void SetsCorrectHeaderNameInEnforcementMode() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .Build(); + + Assert.Equal(CspConstants.CspEnforcedHeaderName, policy.GetHeaderName()); + } + + [Fact] + public void WhenStrictDynamicNotSet_BuildsPolicyCorrectly() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .Build(); + + Assert.DoesNotContain("strict-dynamic", policy.GetPolicy()); + } + + [Fact] + public void WhenStrictDynamicSet_BuildsPolicyCorrectly() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .WithStrictDynamic() + .Build(); + + Assert.Contains("strict-dynamic", policy.GetPolicy()); + } + + [Fact] + public void WhenUnsafeEvalNotSet_BuildsPolicyCorrectly() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .Build(); + + Assert.DoesNotContain("unsafe-eval", policy.GetPolicy()); + } + + [Fact] + public void WhenUnsafeEvalSet_BuildsPolicyCorrectly() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .WithUnsafeEval() + .Build(); + + Assert.Contains("unsafe-eval", policy.GetPolicy()); + } + + // TODO: Add more coverage around reporting URLs + [Fact] + public void WhenReportingUriSet_BuildsPolicyCorrectly() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .WithReportingUri("/cspreport") + .Build(); + + Assert.Contains("report-uri /cspreport", policy.GetPolicy()); + } + + [Fact] + public void AlwaysRestrictsBaseUriAndObjectSrcToNone() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .Build(); + + Assert.Contains("base-uri 'none'; object-src 'none';", policy.GetPolicy()); + } + + [Fact] + public void AlwaysSetsFallbackHttpAndHttpsProtocolsInScriptSrc() + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .Build(); + + Assert.Matches("script-src .* https: http:", policy.GetPolicy()); + } + } +} diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 98c9f736527a..f8383ae71ad4 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -40,8 +40,6 @@ public async Task test() // Assert response.EnsureSuccessStatusCode(); Assert.Single(response.Headers); - var expectedPolicy = "object-src 'none'; script-src 'nonce-{random}' 'strict-dynamic' https: http:; base-uri 'none'; "; - Assert.Equal(expectedPolicy, response.Headers.GetValues(CspConstants.CspHeaderKey).FirstOrDefault()); Assert.Equal("Test response", await response.Content.ReadAsStringAsync()); } } From 24f933f7d62033f2e3a0bc88b6dd969bd698f85e Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 12:16:17 +0000 Subject: [PATCH 08/26] Remove CSP service and policy provider --- src/Middleware/CSP/src/CspMiddleware.cs | 6 +-- src/Middleware/CSP/src/CspOptions.cs | 12 ------ src/Middleware/CSP/src/CspService.cs | 18 -------- .../CSP/src/CspServiceCollectionExtensions.cs | 43 ------------------- .../DefaultContentSecurityPolicyProvider.cs | 17 -------- .../CSP/src/IContentSecurityPolicyProvider.cs | 11 ----- src/Middleware/CSP/src/ICspService.cs | 9 ---- .../CSP/src/LoggingConfiguration.cs | 5 --- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 4 +- 9 files changed, 3 insertions(+), 122 deletions(-) delete mode 100644 src/Middleware/CSP/src/CspOptions.cs delete mode 100644 src/Middleware/CSP/src/CspService.cs delete mode 100644 src/Middleware/CSP/src/CspServiceCollectionExtensions.cs delete mode 100644 src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs delete mode 100644 src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs delete mode 100644 src/Middleware/CSP/src/ICspService.cs diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 69427a81f5d8..75aec9e620d0 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -7,19 +7,17 @@ namespace Microsoft.AspNetCore.Csp public class CspMiddleware { private readonly RequestDelegate _next; - private readonly ICspService _cspService; private readonly ContentSecurityPolicy _csp; - public CspMiddleware(RequestDelegate next, ICspService cspService, ContentSecurityPolicy csp) + public CspMiddleware(RequestDelegate next, ContentSecurityPolicy csp) { _next = next; - _cspService = cspService; _csp = csp; } public Task Invoke(HttpContext context) { - context.Response.Headers[CspConstants.CspEnforcedHeaderName] = _csp.GetPolicy(); + context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(); return _next(context); } } diff --git a/src/Middleware/CSP/src/CspOptions.cs b/src/Middleware/CSP/src/CspOptions.cs deleted file mode 100644 index fdfc1b2dae48..000000000000 --- a/src/Middleware/CSP/src/CspOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Csp -{ - public class CspOptions - { - } -} diff --git a/src/Middleware/CSP/src/CspService.cs b/src/Middleware/CSP/src/CspService.cs deleted file mode 100644 index 7391db6afb34..000000000000 --- a/src/Middleware/CSP/src/CspService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Csp -{ - public class CspService : ICspService - { - public CspService() - { - - } - - public void ApplyResult(HttpResponse response) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Middleware/CSP/src/CspServiceCollectionExtensions.cs b/src/Middleware/CSP/src/CspServiceCollectionExtensions.cs deleted file mode 100644 index 143512871e9d..000000000000 --- a/src/Middleware/CSP/src/CspServiceCollectionExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Csp; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class CspServiceCollectionExtensions - { - public static IServiceCollection AddCsp(this IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - services.AddOptions(); - - services.TryAdd(ServiceDescriptor.Transient()); - services.TryAdd(ServiceDescriptor.Transient()); - - return services; - } - - public static IServiceCollection AddCsp(this IServiceCollection services, Action setupAction) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (setupAction == null) - { - throw new ArgumentNullException(nameof(setupAction)); - } - - services.AddCsp(); - services.Configure(setupAction); - - return services; - } - } -} diff --git a/src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs b/src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs deleted file mode 100644 index b4fc43e120f7..000000000000 --- a/src/Middleware/CSP/src/DefaultContentSecurityPolicyProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Csp -{ - public class DefaultContentSecurityPolicyProvider : IContentSecurityPolicyProvider - { - public Task GetPolicyAsync(HttpContext context) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs b/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs deleted file mode 100644 index 8f4bf6223481..000000000000 --- a/src/Middleware/CSP/src/IContentSecurityPolicyProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Csp -{ - public interface IContentSecurityPolicyProvider - { - - Task GetPolicyAsync(HttpContext context); - } -} diff --git a/src/Middleware/CSP/src/ICspService.cs b/src/Middleware/CSP/src/ICspService.cs deleted file mode 100644 index 47e5e5d628a0..000000000000 --- a/src/Middleware/CSP/src/ICspService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Csp -{ - public interface ICspService - { - void ApplyResult(HttpResponse response); - } -} diff --git a/src/Middleware/CSP/src/LoggingConfiguration.cs b/src/Middleware/CSP/src/LoggingConfiguration.cs index 06d5f4591c75..cc7e5b90ec7d 100644 --- a/src/Middleware/CSP/src/LoggingConfiguration.cs +++ b/src/Middleware/CSP/src/LoggingConfiguration.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index f8383ae71ad4..4c00cb675674 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -27,13 +27,11 @@ public async Task test() { await context.Response.WriteAsync("Test response"); }); - }) - .ConfigureServices(services => services.AddCsp()); + }); using (var server = new TestServer(hostBuilder)) { // Act - // Actual request. var response = await server.CreateRequest("/") .SendAsync("GET"); From 27335ab9ee8fa118ae5111633df990da9b42433d Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 15 Jul 2020 13:06:30 +0000 Subject: [PATCH 09/26] Refactor policy builder to make it easier to register a reporting endpoint and handler --- .../CSP/src/ContentSecurityPolicy.cs | 33 ++++++++++------ .../CSP/src/ContentSecurityPolicyBuilder.cs | 38 ++++++++++++------- .../CSP/src/CspMiddlewareExtensions.cs | 5 +++ .../ContentSecurityPolicyBuilderTest.cs | 11 +----- src/Middleware/Middleware.sln | 5 +-- 5 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index 86679e07867d..8d17fa7cf66c 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -17,25 +17,36 @@ public class ContentSecurityPolicy { private readonly string _baseAndObject = "base-uri 'none'; object-src 'none'"; + private readonly CspMode _cspMode; + private readonly bool _strictDynamic; + private readonly bool _unsafeEval; + private readonly string _reportingUri; + + public ContentSecurityPolicy( + CspMode cspMode, + bool strictDynamic, + bool unsafeEval, + string reportingUri + ) + { + _cspMode = cspMode; + _strictDynamic = strictDynamic; + _unsafeEval = unsafeEval; + _reportingUri = reportingUri; + } + public string GetHeaderName() { - return CspMode == CspMode.REPORTING ? CspConstants.CspReportingHeaderName : CspConstants.CspEnforcedHeaderName; + return _cspMode == CspMode.REPORTING ? CspConstants.CspReportingHeaderName : CspConstants.CspEnforcedHeaderName; } public string GetPolicy() { return string.Format( "script-src 'nonce-random' {0} {1} https: http:; {2}; {3}", - StrictDynamic ? "'strict-dynamic'" : "", - UnsafeEval ? "'unsafe-eval'" : "", + _strictDynamic ? "'strict-dynamic'" : "", + _unsafeEval ? "'unsafe-eval'" : "", _baseAndObject, - ReportingUri != null ? "report-uri " + ReportingUri : ""); + _reportingUri != null ? "report-uri " + _reportingUri : ""); } - - //TODO: Remove getters - public CspMode CspMode { get; internal set; } - public bool StrictDynamic { get; internal set; } - public bool UnsafeEval { get; internal set; } - public string ReportingUri { get; internal set; } - public LoggingConfiguration LoggingConfiguration { get; internal set; } } } diff --git a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs index de2c587de6c4..eaac66e33d3a 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs @@ -4,62 +4,74 @@ namespace Microsoft.AspNetCore.Csp { public class ContentSecurityPolicyBuilder { - private readonly ContentSecurityPolicy _policy = new ContentSecurityPolicy(); - private LoggingConfiguration _LoggingConfiguration; + private LoggingConfiguration _loggingConfiguration; + private CspMode _cspMode; + private bool _strictDynamic; + private bool _unsafeEval; + private string _reportingUri; public ContentSecurityPolicyBuilder() { // TODO: Consider adding builder for logging config - _LoggingConfiguration = new LoggingConfiguration(); - _LoggingConfiguration.LogLevel = Extensions.Logging.LogLevel.Information; + _loggingConfiguration = new LoggingConfiguration(); + _loggingConfiguration.LogLevel = Extensions.Logging.LogLevel.Information; } public ContentSecurityPolicyBuilder WithCspMode(CspMode cspMode) { - _policy.CspMode = cspMode; + _cspMode = cspMode; return this; } public ContentSecurityPolicyBuilder WithStrictDynamic() { - _policy.StrictDynamic = true; + _strictDynamic = true; return this; } public ContentSecurityPolicyBuilder WithUnsafeEval() { - _policy.UnsafeEval = true; + _unsafeEval = true; return this; } public ContentSecurityPolicyBuilder WithReportingUri(string reportingUri) { // TODO: normalize URL - _policy.ReportingUri = reportingUri; + _reportingUri = reportingUri; return this; } public ContentSecurityPolicyBuilder WithLoggingConfiguration(LoggingConfiguration loggingConfiguration) { - _LoggingConfiguration = loggingConfiguration; + _loggingConfiguration = loggingConfiguration; return this; } + public bool HasReporting() + { + return _reportingUri != null; + } + public ContentSecurityPolicy Build() { - if (_policy.CspMode == CspMode.NONE) + if (_cspMode == CspMode.NONE) { // TODO: Error message throw new InvalidOperationException(); } - if (_policy.CspMode == CspMode.REPORTING && _policy.ReportingUri == null) + if (_cspMode == CspMode.REPORTING && _reportingUri == null) { // TODO: Error message throw new InvalidOperationException(); } - _policy.LoggingConfiguration = _LoggingConfiguration; - return _policy; + return new ContentSecurityPolicy( + _cspMode, + _strictDynamic, + _unsafeEval, + _reportingUri + ); } } } diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index f98dcde82d4c..4628f963fc13 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -15,6 +15,11 @@ public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action(policyBuilder.Build()); } } diff --git a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs index c713e451452e..ab04dea23349 100644 --- a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs +++ b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyBuilderTest.cs @@ -17,16 +17,7 @@ public void IfCspModeNotSet_thenExceptionThrown() ); } - [Fact] - public void WhenNoLoggingConfigurationSet_thenDefaultLoggingConfigurationUsed() - { - var policy = new ContentSecurityPolicyBuilder() - .WithCspMode(CspMode.ENFORCING) - .Build(); - - //TODO: Define default logging config - Assert.Equal(LogLevel.Information, policy.LoggingConfiguration.LogLevel); - } + // TODO: Add logging configuration test [Fact] public void WhenModeSetToReporting_IfNoReportingUriSet_thenExceptionThrown() diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index 60e41c20623e..c9a03ac214d4 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -305,8 +305,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSP", "CSP", "{75DC8384-017 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp", "CSP\src\Microsoft.AspNetCore.Csp.csproj", "{73A2989B-5F4F-4095-AC25-C705173FA825}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{723A26AE-C8A3-49D4-8CBE-83FEAB46FFAC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp.Test", "CSP\test\UnitTests\Microsoft.AspNetCore.Csp.Test.csproj", "{A1830D70-5B4C-42F0-BA2D-221F6D9C71B7}" EndProject Global @@ -1805,8 +1803,7 @@ Global {8A9C1F6C-3A47-4868-AA95-3EBE0260F5A0} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472} {80C8E810-1206-482E-BE17-961DD2EBFB11} = {4623F52E-2070-4631-8DEE-7D2F48733FFD} {73A2989B-5F4F-4095-AC25-C705173FA825} = {75DC8384-0171-47AC-8510-7502E3892A73} - {723A26AE-C8A3-49D4-8CBE-83FEAB46FFAC} = {75DC8384-0171-47AC-8510-7502E3892A73} - {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7} = {723A26AE-C8A3-49D4-8CBE-83FEAB46FFAC} + {A1830D70-5B4C-42F0-BA2D-221F6D9C71B7} = {75DC8384-0171-47AC-8510-7502E3892A73} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA} From 5dad8a3e98018a9309084393bc895d4c7b27585d Mon Sep 17 00:00:00 2001 From: Sal Date: Fri, 17 Jul 2020 09:44:01 +0000 Subject: [PATCH 10/26] Initial implementation of CSP reporting endpoint --- .../CSP/src/ContentSecurityPolicyBuilder.cs | 23 +++--- src/Middleware/CSP/src/CspConstants.cs | 1 + .../CSP/src/CspMiddlewareExtensions.cs | 6 +- src/Middleware/CSP/src/CspReport.cs | 31 ++++++++ .../CSP/src/CspReportingMiddleware.cs | 73 +++++++++++++++++++ .../CSP/src/LoggingConfiguration.cs | 4 +- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 41 +++++++++++ 7 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 src/Middleware/CSP/src/CspReport.cs create mode 100644 src/Middleware/CSP/src/CspReportingMiddleware.cs diff --git a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs index eaac66e33d3a..0fe0ca0beeff 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs @@ -1,21 +1,15 @@ using System; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp { public class ContentSecurityPolicyBuilder { - private LoggingConfiguration _loggingConfiguration; private CspMode _cspMode; private bool _strictDynamic; private bool _unsafeEval; private string _reportingUri; - - public ContentSecurityPolicyBuilder() - { - // TODO: Consider adding builder for logging config - _loggingConfiguration = new LoggingConfiguration(); - _loggingConfiguration.LogLevel = Extensions.Logging.LogLevel.Information; - } + private LogLevel _logLevel = LogLevel.Information; public ContentSecurityPolicyBuilder WithCspMode(CspMode cspMode) { @@ -41,9 +35,9 @@ public ContentSecurityPolicyBuilder WithReportingUri(string reportingUri) return this; } - public ContentSecurityPolicyBuilder WithLoggingConfiguration(LoggingConfiguration loggingConfiguration) + public ContentSecurityPolicyBuilder WithLogLevel(LogLevel logLevel) { - _loggingConfiguration = loggingConfiguration; + _logLevel = logLevel; return this; } @@ -52,6 +46,15 @@ public bool HasReporting() return _reportingUri != null; } + public LoggingConfiguration LoggingConfiguration() + { + return new LoggingConfiguration + { + LogLevel = _logLevel, + ReportUri = _reportingUri + }; + } + public ContentSecurityPolicy Build() { if (_cspMode == CspMode.NONE) diff --git a/src/Middleware/CSP/src/CspConstants.cs b/src/Middleware/CSP/src/CspConstants.cs index 688c22f8d115..e738ea203b23 100644 --- a/src/Middleware/CSP/src/CspConstants.cs +++ b/src/Middleware/CSP/src/CspConstants.cs @@ -10,5 +10,6 @@ public static class CspConstants { public static readonly string CspEnforcedHeaderName = "Content-Security-Policy"; public static readonly string CspReportingHeaderName = "Content-Security-Policy-Report-Only"; + public static readonly string CspReportContentType = "application/csp-report"; } } diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index 4628f963fc13..66ec3af04997 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -15,9 +15,13 @@ public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action context.Request.Path.StartsWithSegments(loggingConfig.ReportUri), + appBuilder => appBuilder.UseMiddleware(loggingConfig)); } return app.UseMiddleware(policyBuilder.Build()); diff --git a/src/Middleware/CSP/src/CspReport.cs b/src/Middleware/CSP/src/CspReport.cs new file mode 100644 index 000000000000..ca609873a700 --- /dev/null +++ b/src/Middleware/CSP/src/CspReport.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Csp +{ + public class CspReport + { + public Report ReportData { get; set; } + // TODO: if we find a way to get the csp-report field from the JSON we'll be able to remove one level of nestedness + public class Report + { + public string BlockedUri { get; set; } + public string DocumentUri { get; set; } + public string Referrer { get; set; } + public string ViolatedDirective { get; set; } + public string SourceFile { get; set; } + public int LineNumber { get; set; } + + // Old browsers don't set the next two fields (e.g. Firefox v25/v26) + public string OriginalPolicy { get; set; } + public string EffectiveDirective { get; set; } + + // CSP3 only + public string ScriptSample { get; set; } + public string Disposition { get; set; } + } + } +} diff --git a/src/Middleware/CSP/src/CspReportingMiddleware.cs b/src/Middleware/CSP/src/CspReportingMiddleware.cs new file mode 100644 index 000000000000..6e309e782ecf --- /dev/null +++ b/src/Middleware/CSP/src/CspReportingMiddleware.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Csp +{ + public class CspReportingMiddleware + { + private readonly LoggingConfiguration _loggingConfig; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _serializerOptions; + + public CspReportingMiddleware(RequestDelegate next, LoggingConfiguration loggingConfiguration, ILogger logger) + { + _loggingConfig = loggingConfiguration; + _logger = logger; + _serializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + } + + private bool IsReportRequest(HttpRequest request) + { + // TODO: Is this first condition guaranteed? + return request.Path.StartsWithSegments(_loggingConfig.ReportUri) + && request.ContentType.StartsWith(CspConstants.CspReportContentType) + && request.ContentLength != 0; + } + + private async void HandleIncomingReport(Stream body) + { + try + { + CspReport cspReport = await JsonSerializer.DeserializeAsync(body, _serializerOptions); + _logger.Log(_loggingConfig.LogLevel, TextualizeReport(cspReport, _loggingConfig.LogLevel)); + // TODO: Perhaps catch something more specific + } catch (Exception) + { + return; + } + } + + // TODO: Implement ToString on reportData + private string TextualizeReport(CspReport cspReport, LogLevel logLevel) + { + return cspReport.ReportData.ToString(); + } + + public Task Invoke(HttpContext context) + { + if (IsReportRequest(context.Request)) + { + HandleIncomingReport(context.Request.Body); + } + + context.Response.StatusCode = (int) HttpStatusCode.NoContent; + // TODO: Is there a better way to write an empty response? + return context.Response.WriteAsync(""); + } + } +} diff --git a/src/Middleware/CSP/src/LoggingConfiguration.cs b/src/Middleware/CSP/src/LoggingConfiguration.cs index cc7e5b90ec7d..12a5c83fc649 100644 --- a/src/Middleware/CSP/src/LoggingConfiguration.cs +++ b/src/Middleware/CSP/src/LoggingConfiguration.cs @@ -4,6 +4,8 @@ namespace Microsoft.AspNetCore.Csp { public class LoggingConfiguration { - public LogLevel LogLevel { get; internal set; } + public LogLevel LogLevel { get; set; } + + public string ReportUri { get; set; } } } diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 4c00cb675674..1ec01ed14086 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -6,6 +6,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using System.IO; +using System.Text; namespace Microsoft.AspNetCore.Csp.Test { @@ -38,8 +42,45 @@ public async Task test() // Assert response.EnsureSuccessStatusCode(); Assert.Single(response.Headers); + Assert.NotEmpty(response.Headers.GetValues(CspConstants.CspEnforcedHeaderName).FirstOrDefault()); Assert.Equal("Test response", await response.Content.ReadAsStringAsync()); } } + + [Fact] + public async void ProcessesMalformedReportRequestsCorrectly() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING) + .WithReportingUri("/cspreport"); + }); + app.Run(async context => + { + await context.Response.WriteAsync("Test response"); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + var context = await server.SendAsync(c => + { + c.Request.Method = "POST"; + c.Request.Path = "/cspreport"; + c.Request.Headers[HeaderNames.ContentType] = CspConstants.CspReportContentType; + c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("malformed")); + }); + + // Assert + Assert.Equal(204, context.Response.StatusCode); + Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); + } + } } } From 44de3fd0010ec4aa3da02c3601d50ffd8ea2f43e Mon Sep 17 00:00:00 2001 From: Sal Date: Fri, 17 Jul 2020 12:06:59 +0000 Subject: [PATCH 11/26] Improve marshalling of JSON reports to CspReport object --- src/Middleware/CSP/src/CspReport.cs | 18 +++-- .../CSP/src/CspReportingMiddleware.cs | 8 +- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 77 +++++++++++++++++-- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/src/Middleware/CSP/src/CspReport.cs b/src/Middleware/CSP/src/CspReport.cs index ca609873a700..8ee8ef2107f5 100644 --- a/src/Middleware/CSP/src/CspReport.cs +++ b/src/Middleware/CSP/src/CspReport.cs @@ -1,30 +1,38 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; + +using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.Csp { public class CspReport { + [JsonPropertyName("csp-report")] public Report ReportData { get; set; } // TODO: if we find a way to get the csp-report field from the JSON we'll be able to remove one level of nestedness public class Report { + [JsonPropertyName("blocked-uri")] public string BlockedUri { get; set; } + [JsonPropertyName("document-uri")] public string DocumentUri { get; set; } + [JsonPropertyName("referrer")] public string Referrer { get; set; } + [JsonPropertyName("violated-directive")] public string ViolatedDirective { get; set; } + [JsonPropertyName("source-file")] public string SourceFile { get; set; } + [JsonPropertyName("line-number")] public int LineNumber { get; set; } // Old browsers don't set the next two fields (e.g. Firefox v25/v26) + [JsonPropertyName("original-policy")] public string OriginalPolicy { get; set; } + [JsonPropertyName("effective-directive")] public string EffectiveDirective { get; set; } // CSP3 only + [JsonPropertyName("script-sample")] public string ScriptSample { get; set; } + [JsonPropertyName("disposition")] public string Disposition { get; set; } } } diff --git a/src/Middleware/CSP/src/CspReportingMiddleware.cs b/src/Middleware/CSP/src/CspReportingMiddleware.cs index 6e309e782ecf..377fd6a126f4 100644 --- a/src/Middleware/CSP/src/CspReportingMiddleware.cs +++ b/src/Middleware/CSP/src/CspReportingMiddleware.cs @@ -44,9 +44,11 @@ private async void HandleIncomingReport(Stream body) try { CspReport cspReport = await JsonSerializer.DeserializeAsync(body, _serializerOptions); - _logger.Log(_loggingConfig.LogLevel, TextualizeReport(cspReport, _loggingConfig.LogLevel)); - // TODO: Perhaps catch something more specific - } catch (Exception) + if (cspReport.ReportData != null) + { + _logger.Log(_loggingConfig.LogLevel, TextualizeReport(cspReport, _loggingConfig.LogLevel)); + } + } catch (JsonException) { return; } diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 1ec01ed14086..554915a28f30 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -10,13 +10,17 @@ using Microsoft.Net.Http.Headers; using System.IO; using System.Text; +using Moq; namespace Microsoft.AspNetCore.Csp.Test { public class CspMiddlewareTests { - [Fact] - public async Task test() + [Theory] + [InlineData("/")] + [InlineData("/cheese")] + [InlineData("/foo")] + public async Task cspHeaderIsSetOnAllResponses(string requestPath) { // Arrange var hostBuilder = new WebHostBuilder() @@ -36,7 +40,7 @@ public async Task test() using (var server = new TestServer(hostBuilder)) { // Act - var response = await server.CreateRequest("/") + var response = await server.CreateRequest(requestPath) .SendAsync("GET"); // Assert @@ -47,10 +51,56 @@ public async Task test() } } + [Theory] + [InlineData("GET", "foo")] + [InlineData("GET", "application/csp-report")] + [InlineData("POST", "foo")] + [InlineData("POST", "application/csp-report")] + [InlineData("PUT", "foo")] + [InlineData("PUT", "application/csp-report")] + [InlineData("HEAD", "foo")] + [InlineData("HEAD", "application/csp-report")] + public async void ProcessesMalformedReportRequestsCorrectly(string method, string contentType) + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING) + .WithReportingUri("/cspreport"); + }); + app.Run(async context => + { + await context.Response.WriteAsync("Test response"); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + var context = await server.SendAsync(c => + { + c.Request.Method = method; + c.Request.Path = "/cspreport"; + c.Request.Headers[HeaderNames.ContentType] = contentType; + c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("malformed")); + }); + + // Assert + Assert.Equal(204, context.Response.StatusCode); + Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); + } + } + [Fact] - public async void ProcessesMalformedReportRequestsCorrectly() + public async void ProcessesCspReportRequestsCorrectly() { // Arrange + var logger = new Mock>(); + var hostBuilder = new WebHostBuilder() .Configure(app => { @@ -64,6 +114,10 @@ public async void ProcessesMalformedReportRequestsCorrectly() { await context.Response.WriteAsync("Test response"); }); + }) + .ConfigureServices(services => + { + services.AddSingleton(typeof(ILogger), logger.Object); }); using (var server = new TestServer(hostBuilder)) @@ -74,12 +128,25 @@ public async void ProcessesMalformedReportRequestsCorrectly() c.Request.Method = "POST"; c.Request.Path = "/cspreport"; c.Request.Headers[HeaderNames.ContentType] = CspConstants.CspReportContentType; - c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("malformed")); + c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes( + @"{ + ""csp-report"": { + ""document-uri"": ""http://example.com/signup.html"", + ""referrer"": ""http://evil.com"", + ""blocked-uri"": ""http://example.com/css/style.css"", + ""violated-directive"": ""style-src cdn.example.com"", + ""original-policy"": ""default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports"", + ""disposition"": ""report"" + } + }" + )); }); // Assert Assert.Equal(204, context.Response.StatusCode); Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); + //TODO: ASSERT ON THE LOGGING STATEMENT! + //logger.Verify(m => m.Log(LogLevel.Information, "")); } } } From a82c1878aadaa4dde6e6761155044651fa5075cb Mon Sep 17 00:00:00 2001 From: Aaron Shim <5382864+aaronshim@users.noreply.github.com> Date: Thu, 16 Jul 2020 22:05:32 +0000 Subject: [PATCH 12/26] Scaffolding for an example project that demonstrates CSP with templating. --- .../CspMiddlewareWebSite.csproj | 21 ++++++ .../CspApplication/Pages/Error.cshtml | 26 +++++++ .../CspApplication/Pages/Error.cshtml.cs | 31 ++++++++ .../CspApplication/Pages/Index.cshtml | 26 +++++++ .../CspApplication/Pages/Index.cshtml.cs | 25 ++++++ .../CspApplication/Pages/Privacy.cshtml | 8 ++ .../CspApplication/Pages/Privacy.cshtml.cs | 24 ++++++ .../Pages/Shared/_Layout.cshtml | 48 ++++++++++++ .../Shared/_ValidationScriptsPartial.cshtml | 2 + .../CspApplication/Pages/_ViewImports.cshtml | 3 + .../CspApplication/Pages/_ViewStart.cshtml | 3 + .../test/testassets/CspApplication/README.md | 5 ++ .../test/testassets/CspApplication/Startup.cs | 58 ++++++++++++++ .../appsettings.Development.json | 9 +++ .../CspApplication/appsettings.json | 10 +++ .../CspApplication/wwwroot/css/site.css | 71 ++++++++++++++++++ .../CspApplication/wwwroot/favicon.ico | Bin 0 -> 32038 bytes .../CspApplication/wwwroot/js/example.js | 1 + .../CspApplication/wwwroot/js/site.js | 6 ++ src/Middleware/Middleware.sln | 23 +++++- 20 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml.cs create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml.cs create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml.cs create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewStart.cshtml create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/README.md create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Startup.cs create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/appsettings.Development.json create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/appsettings.json create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/wwwroot/favicon.ico create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/example.js create mode 100644 src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js diff --git a/src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj b/src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj new file mode 100644 index 000000000000..9fa75eeb3355 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj @@ -0,0 +1,21 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + + + + + + + + + + + diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml new file mode 100644 index 000000000000..6f92b9565570 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml.cs b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml.cs new file mode 100644 index 000000000000..aaa2c0635815 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Error.cshtml.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace CspApplication.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml new file mode 100644 index 000000000000..9f1024ee1a33 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml @@ -0,0 +1,26 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + + + + + +
+

Welcome

+

Learn about building Web apps with ASP.NET Core.

+ +
    +
    diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml.cs b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml.cs new file mode 100644 index 000000000000..84d2a7b874da --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace CspApplication.Pages +{ + public class IndexModel : PageModel + { + private readonly ILogger _logger; + + public IndexModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + + } + } +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml new file mode 100644 index 000000000000..46ba96612ec3 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

    @ViewData["Title"]

    + +

    Use this page to detail your site's privacy policy.

    diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml.cs b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml.cs new file mode 100644 index 000000000000..567303dae6e7 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Privacy.cshtml.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace CspApplication.Pages +{ + public class PrivacyModel : PageModel + { + private readonly ILogger _logger; + + public PrivacyModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + } + } +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml new file mode 100644 index 000000000000..1588a79c8564 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml @@ -0,0 +1,48 @@ + + + + + + @ViewData["Title"] - CspApplication + + + + +
    + +
    +
    +
    + @RenderBody() +
    +
    + +
    +
    + © 2020 - CspApplication - Privacy +
    +
    + + + + @RenderSection("Scripts", required: false) + + diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 000000000000..5a16d80a9aa7 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml new file mode 100644 index 000000000000..05729b9555e6 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using CspApplication +@namespace CspApplication.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewStart.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewStart.cshtml new file mode 100644 index 000000000000..a5f10045db97 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/README.md b/src/Middleware/CSP/test/testassets/CspApplication/README.md new file mode 100644 index 000000000000..abe8417fe00f --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/README.md @@ -0,0 +1,5 @@ +This is an example application to show how to set up the CSP middleware and configure the policy, and then have our nonces propagate to the templated HTML. + +## How to run +1. Change the project name to `CspMiddlewareWebSite` and change the `IIS Express` entry to `CspApplication`. +2. If you get build errors that complain about missing DLLs, try `.\build.cmd` from the repo root. If you're still getting missing DLL errors for `Microsoft.AspNetCore.Mvc`, try navingating to `src\Mvc` and then running the `.\build.cmd` there. You might have to install Node and put it on your path for the compilation of ASP.NET MVC to work. diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs new file mode 100644 index 000000000000..b39fdc21204d --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Csp; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace CspApplication +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // Not sure how many of these we absolutely need to do a basic templated HTML page with a reporting endpoint. + app.UseDeveloperExceptionPage(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapRazorPages(); + }); + + // CSP configuration. + app.UseCsp(policyBuilder => policyBuilder.WithCspMode(CspMode.REPORTING) + .WithReportingUri("/csp")); + } + + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/appsettings.Development.json b/src/Middleware/CSP/test/testassets/CspApplication/appsettings.Development.json new file mode 100644 index 000000000000..8983e0fc1c5e --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/appsettings.json b/src/Middleware/CSP/test/testassets/CspApplication/appsettings.json new file mode 100644 index 000000000000..d9d9a9bff6fd --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css new file mode 100644 index 000000000000..e679a8ea7fb5 --- /dev/null +++ b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/favicon.ico b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch literal 32038 zcmeHwX>eTEbtY7aYbrGrkNjgie?1jXjZ#zP%3n{}GObKv$BxI7Sl;Bwl5E+Qtj&t8 z*p|m4DO#HoJC-FyvNnp8NP<{Na0LMnTtO21(rBP}?EAiNjWgeO?z`{3ZoURUQlV2d zY1Pqv{m|X_oO91|?^z!6@@~od!@OH>&BN;>c@O+yUfy5w>LccTKJJ&`-k<%M^Zvi( z<$dKp=jCnNX5Qa+M_%6g|IEv~4R84q9|7E=|Ho(Wz3f-0wPjaRL;W*N^>q%^KGRr7 zxbjSORb_c&eO;oV_DZ7ua!sPH=0c+W;`vzJ#j~-x3uj};50#vqo*0w4!LUqs*UCh9 zvy2S%$#8$K4EOa&e@~aBS65_hc~Mpu=454VT2^KzWqEpBA=ME|O;1cn?8p<+{MKJf zbK#@1wzL44m$k(?85=Obido7=C|xWKe%66$z)NrzRwR>?hK?_bbwT z@Da?lBrBL}Zemo1@!9pYRau&!ld17h{f+UV0sY(R{ET$PBB|-=Nr@l-nY6w8HEAw* zRMIQU`24Jl_IFEPcS=_HdrOP5yf81z_?@M>83Vv65$QFr9nPg(wr`Ke8 zaY4ogdnMA*F7a4Q1_uXadTLUpCk;$ZPRRJ^sMOch;rlbvUGc1R9=u;dr9YANbQ<4Z z#P|Cp9BP$FXNPolgyr1XGt$^lFPF}rmBF5rj1Kh5%dforrP8W}_qJL$2qMBS-#%-|s#BPZBSETsn_EBYcr(W5dq( z@f%}C|iN7)YN`^)h7R?Cg}Do*w-!zwZb9=BMp%Wsh@nb22hA zA{`wa8Q;yz6S)zfo%sl08^GF`9csI9BlGnEy#0^Y3b);M+n<(}6jziM7nhe57a1rj zC@(2ISYBL^UtWChKzVWgf%4LW2Tqg_^7jMw`C$KvU+mcakFjV(BGAW9g%CzSyM;Df z143=mq0oxaK-H;o>F3~zJ<(3-j&?|QBn)WJfP#JR zRuA;`N?L83wQt78QIA$(Z)lGQY9r^SFal;LB^qi`8%8@y+mwcGsf~nv)bBy2S7z~9 z=;X@Gglk)^jpbNz?1;`!J3QUfAOp4U$Uxm5>92iT`mek#$>s`)M>;e4{#%HAAcb^8_Ax%ersk|}# z0bd;ZPu|2}18KtvmIo8`1@H~@2ejwo(5rFS`Z4&O{$$+ch2hC0=06Jh`@p+p8LZzY z&2M~8T6X^*X?yQ$3N5EzRv$(FtSxhW>>ABUyp!{484f8(%C1_y)3D%Qgfl_!sz`LTXOjR&L!zPA0qH_iNS!tY{!^2WfD%uT}P zI<~&?@&))5&hPPHVRl9);TPO>@UI2d!^ksb!$9T96V(F){puTsn(}qt_WXNw4VvHj zf;6A_XCvE`Z@}E-IOaG0rs>K>^=Sr&OgT_p;F@v0VCN0Y$r|Lw1?Wjt`AKK~RT*kJ z2>QPuVgLNcF+XKno;WBv$yj@d_WFJbl*#*V_Cwzo@%3n5%z4g21G*PVZ)wM5$A{klYozmGlB zT@u2+s}=f}25%IA!yNcXUr!!1)z(Nqbhojg0lv@7@0UlvUMT)*r;M$d0-t)Z?B1@qQk()o!4fqvfr_I0r7 zy1(NdkHEj#Yu{K>T#We#b#FD=c1XhS{hdTh9+8gy-vkcdkk*QS@y(xxEMb1w6z<^~ zYcETGfB#ibR#ql0EiD;PR$L&Vrh2uRv5t_$;NxC;>7_S5_OXxsi8udY3BUUdi55Sk zcyKM+PQ9YMA%D1kH1q48OFG(Gbl=FmV;yk8o>k%0$rJ8%-IYsHclnYuTskkaiCGkUlkMY~mx&K}XRlKIW;odWIeuKjtbc^8bBOTqK zjj(ot`_j?A6y_h%vxE9o*ntx#PGrnK7AljD_r58ylE*oy@{IY%+mA^!|2vW_`>`aC{#3`#3;D_$^S^cM zRcF+uTO2sICledvFgNMU@A%M)%8JbSLq{dD|2|2Sg8vvh_uV6*Q?F&rKaV{v_qz&y z`f;stIb?Cb2!Cg7CG91Bhu@D@RaIrq-+o+T2fwFu#|j>lD6ZS9-t^5cx>p|?flqUA z;Cgs#V)O#`Aw4$Kr)L5?|7f4izl!;n0jux}tEW$&&YBXz9o{+~HhoiYDJ`w5BVTl&ARya=M7zdy$FEe}iGBur8XE>rhLj&_yDk5D4n2GJZ07u7%zyAfNtOLn;)M?h*Py-Xtql5aJOtL4U8e|!t? z((sc6&OJXrPdVef^wZV&x=Z&~uA7^ix8rly^rEj?#d&~pQ{HN8Yq|fZ#*bXn-26P^ z5!)xRzYO9{u6vx5@q_{FE4#7BipS#{&J7*>y}lTyV94}dfE%Yk>@@pDe&F7J09(-0|wuI|$of-MRfK51#t@t2+U|*s=W; z!Y&t{dS%!4VEEi$efA!#<<7&04?kB}Soprd8*jYv;-Qj~h~4v>{XX~kjF+@Z7<t?^|i z#>_ag2i-CRAM8Ret^rZt*^K?`G|o>1o(mLkewxyA)38k93`<~4VFI?5VB!kBh%NNU zxb8K(^-MU1ImWQxG~nFB-Un;6n{lQz_FfsW9^H$Xcn{;+W^ZcG$0qLM#eNV=vGE@# z1~k&!h4@T|IiI<47@pS|i?Qcl=XZJL#$JKve;booMqDUYY{(xcdj6STDE=n?;fsS1 ze`h~Q{CT$K{+{t+#*I1=&&-UU8M&}AwAxD-rMa=e!{0gQXP@6azBq9(ji11uJF%@5 zCvV`#*?;ZguQ7o|nH%bm*s&jLej#@B35gy32ZAE0`Pz@#j6R&kN5w{O4~1rhDoU zEBdU)%Nl?8zi|DR((u|gg~r$aLYmGMyK%FO*qLvwxK5+cn*`;O`16c!&&XT{$j~5k zXb^fbh1GT-CI*Nj{-?r7HNg=e3E{6rxuluPXY z5Nm8ktc$o4-^SO0|Es_sp!A$8GVwOX+%)cH<;=u#R#nz;7QsHl;J@a{5NUAmAHq4D zIU5@jT!h?kUp|g~iN*!>jM6K!W5ar0v~fWrSHK@})@6Lh#h)C6F6@)&-+C3(zO! z8+kV|B7LctM3DpI*~EYo>vCj>_?x&H;>y0*vKwE0?vi$CLt zfSJB##P|M2dEUDBPKW=9cY-F;L;h3Fs4E2ERdN#NSL7ctAC z?-}_a{*L@GA7JHJudxtDVA{K5Yh*k(%#x4W7w+^ zcb-+ofbT5ieG+@QG2lx&7!MyE2JWDP@$k`M;0`*d+oQmJ2A^de!3c53HFcfW_Wtv< zKghQ;*FifmI}kE4dc@1y-u;@qs|V75Z^|Q0l0?teobTE8tGl@EB?k#q_wUjypJ*R zyEI=DJ^Z+d*&}B_xoWvs27LtH7972qqMxVFcX9}c&JbeNCXUZM0`nQIkf&C}&skSt z^9fw@b^Hb)!^hE2IJq~~GktG#ZWwWG<`@V&ckVR&r=JAO4YniJewVcG`HF;59}=bf zLyz0uxf6MhuSyH#-^!ZbHxYl^mmBVrx) zyrb8sQ*qBd_WXm9c~Of$&ZP$b^)<~0%nt#7y$1Jg$e}WCK>TeUB{P>|b1FAB?%K7>;XiOfd}JQ`|IP#Vf%kVy zXa4;XFZ+>n;F>uX&3|4zqWK2u3c<>q;tzjsb1;d{u;L$-hq3qe@82(ob<3qom#%`+ z;vzYAs7TIMl_O75BXu|r`Qhc4UT*vN$3Oo0kAC!{f2#HexDy|qUpgTF;k{o6|L>7l z=?`=*LXaow1o;oNNLXsGTrvC)$R&{m=94Tf+2iTT3Y_Or z-!;^0a{kyWtO4vksG_3cyc7HQ0~detf0+2+qxq(e1NS251N}w5iTSrM)`0p8rem!j zZ56hGD=pHI*B+dd)2B`%|9f0goozCSeXPw3 z+58k~sI02Yz#lOneJzYcG)EB0|F+ggC6D|B`6}d0khAK-gz7U3EGT|M_9$ZINqZjwf>P zJCZ=ogSoE`=yV5YXrcTQZx@Un(64*AlLiyxWnCJ9I<5Nc*eK6eV1Mk}ci0*NrJ=t| zCXuJG`#7GBbPceFtFEpl{(lTm`LX=B_!H+& z>$*Hf}}y zkt@nLXFG9%v**s{z&{H4e?aqp%&l#oU8lxUxk2o%K+?aAe6jLojA& z_|J0<-%u^<;NT*%4)n2-OdqfctSl6iCHE?W_Q2zpJken#_xUJlidzs249H=b#g z?}L4-Tnp6)t_5X?_$v)vz`s9@^BME2X@w<>sKZ3=B{%*B$T5Nj%6!-Hr;I!Scj`lH z&2dHFlOISwWJ&S2vf~@I4i~(0*T%OFiuX|eD*nd2utS4$1_JM?zmp>a#CsVy6Er^z zeNNZZDE?R3pM?>~e?H_N`C`hy%m4jb;6L#8=a7l>3eJS2LGgEUxsau-Yh9l~o7=Yh z2mYg3`m5*3Ik|lKQf~euzZlCWzaN&=vHuHtOwK!2@W6)hqq$Zm|7`Nmu%9^F6UH?+ z@2ii+=iJ;ZzhiUKu$QB()nKk3FooI>Jr_IjzY6=qxYy;&mvi7BlQ?t4kRjIhb|2q? zd^K~{-^cxjVSj?!Xs=Da5IHmFzRj!Kzh~b!?`P7c&T9s77VLYB?8_?F zauM^)p;qFG!9PHLfIsnt43UnmV?Wn?Ki7aXSosgq;f?MYUuSIYwOn(5vWhb{f%$pn z4ySN-z}_%7|B);A@PA5k*7kkdr4xZ@s{e9j+9w;*RFm;XPDQwx%~;8iBzSKTIGKO z{53ZZU*OLr@S5=k;?CM^i#zkxs3Sj%z0U`L%q`qM+tP zX$aL;*^g$7UyM2Go+_4A+f)IQcy^G$h2E zb?nT$XlgTEFJI8GN6NQf%-eVn9mPilRqUbT$pN-|;FEjq@Ao&TxpZg=mEgBHB zU@grU;&sfmqlO=6|G3sU;7t8rbK$?X0y_v9$^{X`m4jZ_BR|B|@?ZCLSPPEzz`w1n zP5nA;4(kQFKm%$enjkkBxM%Y}2si&d|62L)U(dCzCGn56HN+i#6|nV-TGIo0;W;`( zW-y=1KF4dp$$mC_|6}pbb>IHoKQeZajXQB>jVR?u`R>%l1o54?6NnS*arpVopdEF; zeC5J3*M0p`*8lif;!irrcjC?(uExejsi~>4wKYwstGY^N@KY}TujLx`S=Cu+T=!dx zKWlPm->I**E{A*q-Z^FFT5$G%7Ij0_*Mo4-y6~RmyTzUB&lfae(WZfO>um}mnsDXPEbau-!13!!xd!qh*{C)6&bz0j1I{>y$D-S)b*)JMCPk!=~KL&6Ngin0p6MCOxF2L_R9t8N!$2Wpced<#`y!F;w zKTi5V_kX&X09wAIJ#anfg9Dhn0s7(C6Nj3S-mVn(i|C6ZAVq0$hE)874co};g z^hR7pe4lU$P;*ggYc4o&UTQC%liCXooIfkI3TNaBV%t~FRr}yHu7kjQ2J*3;e%;iW zvDVCh8=G80KAeyhCuY2LjrC!Od1rvF7h}zszxGV)&!)6ChP5WAjv-zQAMNJIG!JHS zwl?pLxC-V5II#(hQ`l)ZAp&M0xd4%cxmco*MIk?{BD=BK`1vpc}D39|XlV z{c&0oGdDa~TL2FT4lh=~1NL5O-P~0?V2#ie`v^CnANfGUM!b4F=JkCwd7Q`c8Na2q zJGQQk^?6w}Vg9-{|2047((lAV84uN%sK!N2?V(!_1{{v6rdgZl56f0zDMQ+q)jKzzu^ztsVken;=DjAh6G`Cw`Q4G+BjS+n*=KI~^K{W=%t zbD-rN)O4|*Q~@<#@1Vx$E!0W9`B~IZeFn87sHMXD>$M%|Bh93rdGf1lKoX3K651t&nhsl= zXxG|%@8}Bbrlp_u#t*DZX<}_0Yb{A9*1Pd_)LtqNwy6xT4pZrOY{s?N4)pPwT(i#y zT%`lRi8U#Ken4fw>H+N`{f#FF?ZxFlLZg7z7#cr4X>id z{9kUD`d2=w_Zlb{^c`5IOxWCZ1k<0T1D1Z31IU0Q2edsZ1K0xv$pQVYq2KEp&#v#Z z?{m@Lin;*Str(C2sfF^L>{R3cjY`~#)m>Wm$Y|1fzeS0-$(Q^z@} zEO*vlb-^XK9>w&Ef^=Zzo-1AFSP#9zb~X5_+){$(eB4K z8gtW+nl{q+CTh+>v(gWrsP^DB*ge(~Q$AGxJ-eYc1isti%$%nM<_&Ev?%|??PK`$p z{f-PM{Ym8k<$$)(F9)tqzFJ?h&Dk@D?Dt{4CHKJWLs8$zy6+(R)pr@0ur)xY{=uXFFzH_> z-F^tN1y(2hG8V)GpDg%wW0Px_ep~nIjD~*HCSxDi0y`H!`V*~RHs^uQsb1*bK1qGpmd zB1m`Cjw0`nLBF2|umz+a#2X$c?Lj;M?Lj;MUp*d>7j~ayNAyj@SLpeH`)BgRH}byy zyQSat!;U{@O(<<2fp&oQkIy$z`_CQ-)O@RN;QD9T4y|wIJ^%U#(BF%=`i49}j!D-) zkOwPSJaG03SMkE~BzW}b_v>LA&y)EEYO6sbdnTX*$>UF|JhZ&^MSb4}Tgbne_4n+C zwI8U4i~PI>7a3{kVa8|))*%C0|K+bIbmV~a`|G#+`TU#g zXW;bWIcWsQi9c4X*RUDpIfyoPY)2bI-r9)xulm1CJDkQd6u+f)_N=w1ElgEBjprPF z3o?Ly0RVeY_{3~fPVckRMxe2lM8hj!B8F)JO z!`AP6>u>5Y&3o9t0QxBpNE=lJx#NyIbp1gD zzUYBIPYHIv9ngk-Zt~<)62^1Zs1LLYMh@_tP^I7EX-9)Ed0^@y{k65Gp0KRcTmMWw zU|+)qx{#q0SL+4q?Q`i0>COIIF8a0Cf&C`hbMj?LmG9K&iW-?PJt*u)38tTXAP>@R zZL6uH^!RYNq$p>PKz7f-zvg>OKXcZ8h!%Vo@{VUZp|+iUD_xb(N~G|6c#oQK^nHZU zKg#F6<)+`rf~k*Xjjye+syV{bwU2glMMMs-^ss4`bYaVroXzn`YQUd__UlZL_mLs z(vO}k!~(mi|L+(5&;>r<;|OHnbXBE78LruP;{yBxZ6y7K3)nMo-{6PCI7gQi6+rF_ zkPod!Z8n}q46ykrlQS|hVB(}(2Kf7BCZ>Vc;V>ccbk2~NGaf6wGQH@W9&?Zt3v(h*P4xDrN>ex7+jH*+Qg z%^jH$&+*!v{sQ!xkWN4+>|b}qGvEd6ANzgqoVy5Qfws}ef2QqF{iiR5{pT}PS&yjo z>lron#va-p=v;m>WB+XVz|o;UJFdjo5_!RRD|6W{4}A2a#bZv)gS_`b|KsSH)Sd_JIr%<%n06TX&t{&!H#{)?4W9hlJ`R1>FyugOh3=D_{einr zu(Wf`qTkvED+gEULO0I*Hs%f;&=`=X4;N8Ovf28x$A*11`dmfy2=$+PNqX>XcG`h% zJY&A6@&)*WT^rC(Caj}2+|X|6cICm5h0OK0cGB_!wEKFZJU)OQ+TZ1q2bTx9hxnq& z$9ee|f9|0M^)#E&Pr4)f?o&DMM4w>Ksb{hF(0|wh+5_{vPow{V%TFzU2za&gjttNi zIyR9qA56dX52Qbv2aY^g`U7R43-p`#sO1A=KS2aKgfR+Yu^bQ*i-qu z%0mP;Ap)B~zZgO9lG^`325gOf?iUHF{~7jyGC)3L(eL(SQ70VzR~wLN18tnx(Cz2~ zctBl1kI)wAe+cxWHw*NW-d;=pd+>+wd$a@GBju*wFvabSaPtHiT!o#QFC+wBVwYo3s=y;z1jM+M=Fj!FZM>UzpL-eZzOT( zhmZmEfWa=%KE#V3-ZK5#v!Hzd{zc^{ctF~- z>DT-U`}5!fk$aj24`#uGdB7r`>oX5tU|d*b|N3V1lXmv%MGrvE(dXG)^-J*LA>$LE z7kut4`zE)v{@Op|(|@i#c>tM!12FQh?}PfA0`Bp%=%*RiXVzLDXnXtE@4B)5uR}a> zbNU}q+712pIrM`k^odG8dKtG$zwHmQI^c}tfjx5?egx3!e%JRm_64e+>`Ra1IRfLb z1KQ`SxmH{cZfyVS5m(&`{V}Y4j6J{b17`h6KWqZ&hfc(oR zxM%w!$F(mKy05kY&lco3%zvLCxBW+t*rxO+i=qGMvobx0-<7`VUu)ka`){=ew+Ovt zg%52_{&UbkUA8aJPWsk)gYWV4`dnxI%s?7^fGpq{ZQuu=VH{-t7w~K%_E<8`zS;V- zKTho*>;UQQul^1GT^HCt@I-q?)&4!QDgBndn?3sNKYKCQFU4LGKJ$n@Je$&w9@E$X z^p@iJ(v&`1(tq~1zc>0Vow-KR&vm!GUzT?Eqgnc)leZ9p)-Z*C!zqb=-$XG0 z^!8RfuQs5s>Q~qcz92(a_Q+KH?C*vCTr~UdTiR`JGuNH8v(J|FTiSEcPrBpmHRtmd zI2Jng0J=bXK);YY^rM?jzn?~X-Pe`GbAy{D)Y6D&1GY-EBcy%Bq?bKh?A>DD9DD!p z?{q02wno2sraGUkZv5dx+J8)&K$)No43Zr(*S`FEdL!4C)}WE}vJd%{S6-3VUw>Wp z?Aasv`T0^%P$2vE?L+Qhj~qB~K%eW)xH(=b_jU}TLD&BP*Pc9hz@Z=e0nkpLkWl}> z_5J^i(9Z7$(XG9~I3sY)`OGZ#_L06+Dy4E>UstcP-rU@xJ$&rxvo!n1Ao`P~KLU-8 z{zDgN4-&A6N!kPSYbQ&7sLufi`YtE2uN$S?e&5n>Y4(q#|KP!cc1j)T^QrUXMPFaP z_SoYO8S8G}Z$?AL4`;pE?7J5K8yWqy23>cCT2{=-)+A$X^-I9=e!@J@A&-;Ufc)`H}c(VI&;0x zrrGv()5mjP%jXzS{^|29?bLNXS0bC%p!YXI!;O457rjCEEzMkGf~B3$T}dXBO23tP z+Ci>;5UoM?C@bU@f9G1^X3=ly&ZeFH<@|RnOG--A&)fd)AUgjw?%izq{p(KJ`EP0v z2mU)P!+3t@X14DA=E2RR-|p${GZ9ETX=d+kJRZL$nSa0daI@&oUUxnZg0xd_xu>Vz lzF#z5%kSKX?YLH3ll^(hI(_`L*t#Iva2Ede*Z;>H_ Date: Fri, 17 Jul 2020 15:15:12 +0000 Subject: [PATCH 13/26] Update project structure to reflect changes in upstream --- AspNetCore.sln | 172 +++++++++++++++++++++++++++++++------------------ 1 file changed, 110 insertions(+), 62 deletions(-) diff --git a/AspNetCore.sln b/AspNetCore.sln index aea9643f478f..37a614dc99aa 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -321,11 +321,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Hostin EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server.IntegrationTesting", "Server.IntegrationTesting", "{F4B3C10B-F713-45D1-84EF-DD503BA09D20}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Server.IntegrationTesting", "src\Hosting\Server.IntegrationTesting\src\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", "{7AA8771B-F1F3-409E-960A-EEF00B94135A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IntegrationTesting", "src\Hosting\Server.IntegrationTesting\src\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", "{7AA8771B-F1F3-409E-960A-EEF00B94135A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestHost", "TestHost", "{E6639EAB-40B0-4BB6-A56B-25ECC4B6D3D9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.TestHost", "src\Hosting\TestHost\src\Microsoft.AspNetCore.TestHost.csproj", "{0B490CDE-C350-41A0-9DEA-CDA5FB72205D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.TestHost", "src\Hosting\TestHost\src\Microsoft.AspNetCore.TestHost.csproj", "{0B490CDE-C350-41A0-9DEA-CDA5FB72205D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication", "Authentication", "{822D1519-77F0-484A-B9AB-F694C2CC25F1}" EndProject @@ -639,7 +639,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebUti EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metadata", "Metadata", "{71DE18B5-87F9-4D03-89BB-C3E243576F92}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Metadata", "src\Http\Metadata\src\Microsoft.AspNetCore.Metadata.csproj", "{3A554C0B-2956-4152-BF31-2C8CAE5D2717}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Metadata", "src\Http\Metadata\src\Microsoft.AspNetCore.Metadata.csproj", "{3A554C0B-2956-4152-BF31-2C8CAE5D2717}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{9F21A235-436E-4020-A076-1DF4F89D0CA0}" EndProject @@ -703,11 +703,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Cookie EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Diagnostics.EntityFrameworkCore", "Diagnostics.EntityFrameworkCore", "{3D551023-1D98-479D-A41E-EBD0C05A06FF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore", "src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj", "{861CA437-6402-4E45-9E1F-35A509E61568}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore", "src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj", "{861CA437-6402-4E45-9E1F-35A509E61568}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rewrite", "Rewrite", "{049030CA-4D78-4B24-A112-28C61AB2509C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Rewrite", "src\Middleware\Rewrite\src\Microsoft.AspNetCore.Rewrite.csproj", "{5EE9EEE3-C472-4A6D-8DB8-C82998584E2C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Rewrite", "src\Middleware\Rewrite\src\Microsoft.AspNetCore.Rewrite.csproj", "{5EE9EEE3-C472-4A6D-8DB8-C82998584E2C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Facebook", "Facebook", "{B714B9AB-0A8F-4FB0-902E-6B73CA60DAB8}" EndProject @@ -723,169 +723,169 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Authen EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{A127217D-26AF-43C4-96A0-7080EB6F1D65}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoApp", "src\Middleware\WebSockets\samples\EchoApp\EchoApp.csproj", "{19BE6DF6-24F5-4F58-86C8-A9D8368427A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoApp", "src\Middleware\WebSockets\samples\EchoApp\EchoApp.csproj", "{19BE6DF6-24F5-4F58-86C8-A9D8368427A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSockets.TestServer", "src\Middleware\WebSockets\samples\TestServer\WebSockets.TestServer.csproj", "{68E93233-6FF3-41FE-8161-3B7F457473C8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSockets.TestServer", "src\Middleware\WebSockets\samples\TestServer\WebSockets.TestServer.csproj", "{68E93233-6FF3-41FE-8161-3B7F457473C8}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8AAFAD2A-B273-4DB5-8DC9-3CA832248A82}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebSockets.ConformanceTests", "src\Middleware\WebSockets\test\ConformanceTests\Microsoft.AspNetCore.WebSockets.ConformanceTests.csproj", "{897EECDD-628F-4233-B1BF-A13F687057BC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.ConformanceTests", "src\Middleware\WebSockets\test\ConformanceTests\Microsoft.AspNetCore.WebSockets.ConformanceTests.csproj", "{897EECDD-628F-4233-B1BF-A13F687057BC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebSockets.Tests", "src\Middleware\WebSockets\test\UnitTests\Microsoft.AspNetCore.WebSockets.Tests.csproj", "{E7F7B6E9-3374-4C37-8FDA-CB3682442D51}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.Tests", "src\Middleware\WebSockets\test\UnitTests\Microsoft.AspNetCore.WebSockets.Tests.csproj", "{E7F7B6E9-3374-4C37-8FDA-CB3682442D51}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{41B519F7-CF19-4FB4-B96C-E38A7EF45F70}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Diagnostics.FunctionalTests", "src\Middleware\Diagnostics\test\FunctionalTests\Diagnostics.FunctionalTests.csproj", "{122907A1-AD5F-40B3-9D61-05A3F8CF859D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostics.FunctionalTests", "src\Middleware\Diagnostics\test\FunctionalTests\Diagnostics.FunctionalTests.csproj", "{122907A1-AD5F-40B3-9D61-05A3F8CF859D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagnostics.Tests", "src\Middleware\Diagnostics\test\UnitTests\Microsoft.AspNetCore.Diagnostics.Tests.csproj", "{5A3DF44F-53B3-4EC0-A0EF-EFE78190C48D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.Tests", "src\Middleware\Diagnostics\test\UnitTests\Microsoft.AspNetCore.Diagnostics.Tests.csproj", "{5A3DF44F-53B3-4EC0-A0EF-EFE78190C48D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{364376C6-E886-482E-B79C-366E5DAB8A5B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Diagnostics.EFCore.FunctionalTests", "src\Middleware\Diagnostics.EntityFrameworkCore\test\FunctionalTests\Diagnostics.EFCore.FunctionalTests.csproj", "{BDC1141A-DA5D-48D4-B0A7-61612D4F8024}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostics.EFCore.FunctionalTests", "src\Middleware\Diagnostics.EntityFrameworkCore\test\FunctionalTests\Diagnostics.EFCore.FunctionalTests.csproj", "{BDC1141A-DA5D-48D4-B0A7-61612D4F8024}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests", "src\Middleware\Diagnostics.EntityFrameworkCore\test\UnitTests\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.csproj", "{4D08EC2C-A488-414E-8739-341E197773DB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests", "src\Middleware\Diagnostics.EntityFrameworkCore\test\UnitTests\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.csproj", "{4D08EC2C-A488-414E-8739-341E197773DB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MiddlewareAnalysis", "MiddlewareAnalysis", "{07AAFECB-4C42-4533-80D5-0452989FA0F7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{81143573-811E-4F61-9331-6B7173DBA67C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiddlewareAnalysisSample", "src\Middleware\MiddlewareAnalysis\samples\MiddlewareAnalysisSample\MiddlewareAnalysisSample.csproj", "{F4160B93-03F2-4C0F-BEBA-C89F02CACD17}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiddlewareAnalysisSample", "src\Middleware\MiddlewareAnalysis\samples\MiddlewareAnalysisSample\MiddlewareAnalysisSample.csproj", "{F4160B93-03F2-4C0F-BEBA-C89F02CACD17}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.MiddlewareAnalysis", "src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", "{35AA4E31-D157-4AD2-B04F-EE3956795420}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.MiddlewareAnalysis", "src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", "{35AA4E31-D157-4AD2-B04F-EE3956795420}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.MiddlewareAnalysis.Tests", "src\Middleware\MiddlewareAnalysis\test\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj", "{21F89E50-DF06-435E-872B-7639BCE695DF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.MiddlewareAnalysis.Tests", "src\Middleware\MiddlewareAnalysis\test\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj", "{21F89E50-DF06-435E-872B-7639BCE695DF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HealthChecks", "HealthChecks", "{DF86A07D-A370-4CBD-97DD-DCB09466545F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks", "src\Middleware\HealthChecks\src\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj", "{8C828211-F2EC-4BFD-B922-98F73D9DAB5E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks", "src\Middleware\HealthChecks\src\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj", "{8C828211-F2EC-4BFD-B922-98F73D9DAB5E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8EFA9CC7-D03D-4535-898C-F3B80BB19F21}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests", "src\Middleware\HealthChecks\test\UnitTests\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj", "{2E690E42-B20F-4702-9478-1707EEC7C02F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests", "src\Middleware\HealthChecks\test\UnitTests\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj", "{2E690E42-B20F-4702-9478-1707EEC7C02F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HealthChecks.EntityFrameworkCore", "HealthChecks.EntityFrameworkCore", "{9C7F31F8-B782-4016-9B0E-72C8CB42334C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore", "src\Middleware\HealthChecks.EntityFrameworkCore\src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj", "{6DEB6C24-3A35-4E72-BCA8-33344EEE0119}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore", "src\Middleware\HealthChecks.EntityFrameworkCore\src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj", "{6DEB6C24-3A35-4E72-BCA8-33344EEE0119}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests", "src\Middleware\HealthChecks.EntityFrameworkCore\test\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj", "{922869D9-B9FF-4828-8C6F-FCA7963DB011}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests", "src\Middleware\HealthChecks.EntityFrameworkCore\test\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj", "{922869D9-B9FF-4828-8C6F-FCA7963DB011}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostFilteringSample", "src\Middleware\HostFiltering\sample\HostFilteringSample.csproj", "{ECF7A440-D5FC-473F-958C-FE27643844D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostFilteringSample", "src\Middleware\HostFiltering\sample\HostFilteringSample.csproj", "{ECF7A440-D5FC-473F-958C-FE27643844D8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.HostFiltering.Tests", "src\Middleware\HostFiltering\test\Microsoft.AspNetCore.HostFiltering.Tests.csproj", "{92820F5C-4018-4F10-A14B-0AEE619202EE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HostFiltering.Tests", "src\Middleware\HostFiltering\test\Microsoft.AspNetCore.HostFiltering.Tests.csproj", "{92820F5C-4018-4F10-A14B-0AEE619202EE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpOverridesSample", "src\Middleware\HttpOverrides\sample\HttpOverridesSample.csproj", "{2C76F024-8C85-4672-AC23-ACEA0FA5E9B5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpOverridesSample", "src\Middleware\HttpOverrides\sample\HttpOverridesSample.csproj", "{2C76F024-8C85-4672-AC23-ACEA0FA5E9B5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.HttpOverrides.Tests", "src\Middleware\HttpOverrides\test\Microsoft.AspNetCore.HttpOverrides.Tests.csproj", "{C520CDA2-BA0C-4427-8B26-F6B3C2FEB5A5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpOverrides.Tests", "src\Middleware\HttpOverrides\test\Microsoft.AspNetCore.HttpOverrides.Tests.csproj", "{C520CDA2-BA0C-4427-8B26-F6B3C2FEB5A5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpsPolicySample", "src\Middleware\HttpsPolicy\sample\HttpsPolicySample.csproj", "{91DD8440-7A66-4880-A0CE-BC6009D2DCFF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpsPolicySample", "src\Middleware\HttpsPolicy\sample\HttpsPolicySample.csproj", "{91DD8440-7A66-4880-A0CE-BC6009D2DCFF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.HttpsPolicy.Tests", "src\Middleware\HttpsPolicy\test\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", "{A837D64D-435A-473F-8828-8E1DD95D8E4B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsPolicy.Tests", "src\Middleware\HttpsPolicy\test\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", "{A837D64D-435A-473F-8828-8E1DD95D8E4B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResponseCompressionSample", "src\Middleware\ResponseCompression\sample\ResponseCompressionSample.csproj", "{4BA8A846-5B24-4F46-90B9-FFEFB889C028}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResponseCompressionSample", "src\Middleware\ResponseCompression\sample\ResponseCompressionSample.csproj", "{4BA8A846-5B24-4F46-90B9-FFEFB889C028}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCompression.Tests", "src\Middleware\ResponseCompression\test\Microsoft.AspNetCore.ResponseCompression.Tests.csproj", "{3CCE49CA-4145-401F-8963-AC28F0908678}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCompression.Tests", "src\Middleware\ResponseCompression\test\Microsoft.AspNetCore.ResponseCompression.Tests.csproj", "{3CCE49CA-4145-401F-8963-AC28F0908678}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RewriteSample", "src\Middleware\Rewrite\sample\RewriteSample.csproj", "{A1628BC4-134A-4854-9F98-B841520F1497}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RewriteSample", "src\Middleware\Rewrite\sample\RewriteSample.csproj", "{A1628BC4-134A-4854-9F98-B841520F1497}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Rewrite.Tests", "src\Middleware\Rewrite\test\Microsoft.AspNetCore.Rewrite.Tests.csproj", "{C69E36EC-3972-4CDF-A467-94CBD7C691A1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Rewrite.Tests", "src\Middleware\Rewrite\test\Microsoft.AspNetCore.Rewrite.Tests.csproj", "{C69E36EC-3972-4CDF-A467-94CBD7C691A1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalizationSample", "src\Middleware\Localization\sample\LocalizationSample.csproj", "{B4FCACA3-FD16-4B89-9D3D-E19C141CE926}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalizationSample", "src\Middleware\Localization\sample\LocalizationSample.csproj", "{B4FCACA3-FD16-4B89-9D3D-E19C141CE926}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C9E481E5-F77D-4462-8E85-1F7FF2DCAA97}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Localization.FunctionalTests", "src\Middleware\Localization\test\FunctionalTests\Microsoft.AspNetCore.Localization.FunctionalTests.csproj", "{5930AA76-C824-45F0-9D30-7EE4CB231E7A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Localization.FunctionalTests", "src\Middleware\Localization\test\FunctionalTests\Microsoft.AspNetCore.Localization.FunctionalTests.csproj", "{5930AA76-C824-45F0-9D30-7EE4CB231E7A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Localization.Tests", "src\Middleware\Localization\test\UnitTests\Microsoft.AspNetCore.Localization.Tests.csproj", "{5BE39E84-020F-405D-99AF-5D46928785AF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Localization.Tests", "src\Middleware\Localization\test\UnitTests\Microsoft.AspNetCore.Localization.Tests.csproj", "{5BE39E84-020F-405D-99AF-5D46928785AF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Localization.Routing", "Localization.Routing", "{8C289D6B-712F-442F-A010-218784BFA4D4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Localization.Routing", "src\Middleware\Localization.Routing\src\Microsoft.AspNetCore.Localization.Routing.csproj", "{D8DE9D37-4733-45F0-9F9D-B5A184CA1ED8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Localization.Routing", "src\Middleware\Localization.Routing\src\Microsoft.AspNetCore.Localization.Routing.csproj", "{D8DE9D37-4733-45F0-9F9D-B5A184CA1ED8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Localization.Routing.Tests", "src\Middleware\Localization.Routing\test\Microsoft.AspNetCore.Localization.Routing.Tests.csproj", "{99CB7560-3B6F-447B-880A-C5A53F1E56F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Localization.Routing.Tests", "src\Middleware\Localization.Routing\test\Microsoft.AspNetCore.Localization.Routing.Tests.csproj", "{99CB7560-3B6F-447B-880A-C5A53F1E56F9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCompression.Performance", "src\Middleware\ResponseCompression\perf\Microsoft.AspNetCore.ResponseCompression.Performance.csproj", "{605D0C19-6E99-4A73-B336-1AD700AE93C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCompression.Performance", "src\Middleware\ResponseCompression\perf\Microsoft.AspNetCore.ResponseCompression.Performance.csproj", "{605D0C19-6E99-4A73-B336-1AD700AE93C0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConformanceTests", "ConformanceTests", "{73BC406B-4D98-4BE0-A756-19F0F3D2B587}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutobahnTestApp", "src\Middleware\WebSockets\test\ConformanceTests\AutobahnTestApp\AutobahnTestApp.csproj", "{2292C14D-151C-4AB7-AA91-7F0165E2073E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutobahnTestApp", "src\Middleware\WebSockets\test\ConformanceTests\AutobahnTestApp\AutobahnTestApp.csproj", "{2292C14D-151C-4AB7-AA91-7F0165E2073E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{45211BB8-4F51-427D-ADA2-7478E3398A60}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Cors.Test", "src\Middleware\CORS\test\UnitTests\Microsoft.AspNetCore.Cors.Test.csproj", "{F8410622-2764-4B05-BEC4-8D25CABBC45A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors.Test", "src\Middleware\CORS\test\UnitTests\Microsoft.AspNetCore.Cors.Test.csproj", "{F8410622-2764-4B05-BEC4-8D25CABBC45A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{2E4D61F1-F504-450E-B4C2-11C5214E7200}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticFileSample", "src\Middleware\StaticFiles\samples\StaticFileSample\StaticFileSample.csproj", "{5EC5F788-458B-4957-8E33-ADD82DB6E420}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StaticFileSample", "src\Middleware\StaticFiles\samples\StaticFileSample\StaticFileSample.csproj", "{5EC5F788-458B-4957-8E33-ADD82DB6E420}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{491D5D41-4424-4642-AF7C-A7DD186CD282}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.StaticFiles.FunctionalTests", "src\Middleware\StaticFiles\test\FunctionalTests\Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj", "{7135BA54-DE2E-42CD-8261-1A88907843FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.StaticFiles.FunctionalTests", "src\Middleware\StaticFiles\test\FunctionalTests\Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj", "{7135BA54-DE2E-42CD-8261-1A88907843FC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.StaticFiles.Tests", "src\Middleware\StaticFiles\test\UnitTests\Microsoft.AspNetCore.StaticFiles.Tests.csproj", "{A72BB350-8903-4914-9583-A156B66419EB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.StaticFiles.Tests", "src\Middleware\StaticFiles\test\UnitTests\Microsoft.AspNetCore.StaticFiles.Tests.csproj", "{A72BB350-8903-4914-9583-A156B66419EB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ResponseCaching", "ResponseCaching", "{7CBB1320-BF86-4B32-9D2B-273A45057727}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching", "src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj", "{186AF4F9-9AFF-4395-9340-006F78D85DFB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching", "src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj", "{186AF4F9-9AFF-4395-9340-006F78D85DFB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F6BC5402-310D-4B1F-A018-99FD1D6C183A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResponseCachingSample", "src\Middleware\ResponseCaching\samples\ResponseCachingSample\ResponseCachingSample.csproj", "{6BB8474D-B8E1-4B77-AA44-253ACA5E686D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResponseCachingSample", "src\Middleware\ResponseCaching\samples\ResponseCachingSample\ResponseCachingSample.csproj", "{6BB8474D-B8E1-4B77-AA44-253ACA5E686D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "src\Middleware\ResponseCaching\test\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{8E5588B6-0557-41CF-9A0B-1A0B412754E1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "src\Middleware\ResponseCaching\test\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{8E5588B6-0557-41CF-9A0B-1A0B412754E1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Session", "Session", "{48BEABD3-2446-466C-8694-D34EF0949369}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SessionSample", "src\Middleware\Session\samples\SessionSample.csproj", "{F0F3E6B1-5BB5-444E-B965-F2A9FFAB26CF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SessionSample", "src\Middleware\Session\samples\SessionSample.csproj", "{F0F3E6B1-5BB5-444E-B965-F2A9FFAB26CF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Session", "src\Middleware\Session\src\Microsoft.AspNetCore.Session.csproj", "{609FEA30-36D6-4FEB-AF07-7BE2BCD4D00B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Session", "src\Middleware\Session\src\Microsoft.AspNetCore.Session.csproj", "{609FEA30-36D6-4FEB-AF07-7BE2BCD4D00B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Session.Tests", "src\Middleware\Session\test\Microsoft.AspNetCore.Session.Tests.csproj", "{A01B523B-35CA-4C14-B792-3887F8741E99}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Session.Tests", "src\Middleware\Session\test\Microsoft.AspNetCore.Session.Tests.csproj", "{A01B523B-35CA-4C14-B792-3887F8741E99}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpSys", "HttpSys", "{166E48ED-9738-4E13-8618-0D805F6F0F65}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Server.HttpSys", "src\Servers\HttpSys\src\Microsoft.AspNetCore.Server.HttpSys.csproj", "{AC0CBDEB-B750-4B81-AEC3-F218A384FB16}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.HttpSys", "src\Servers\HttpSys\src\Microsoft.AspNetCore.Server.HttpSys.csproj", "{AC0CBDEB-B750-4B81-AEC3-F218A384FB16}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{81AF139E-F3BB-46FD-B8DB-93A645E5222C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NodeServicesExamples", "src\Middleware\NodeServices\samples\NodeServicesExamples\NodeServicesExamples.csproj", "{49EAD781-92BF-4863-9159-08674548D1BE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NodeServicesExamples", "src\Middleware\NodeServices\samples\NodeServicesExamples\NodeServicesExamples.csproj", "{49EAD781-92BF-4863-9159-08674548D1BE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.NodeServices.Tests", "src\Middleware\NodeServices\test\Microsoft.AspNetCore.NodeServices.Tests.csproj", "{F7E4CC45-B553-4D58-8B3E-B9F426FAF67F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.NodeServices.Tests", "src\Middleware\NodeServices\test\Microsoft.AspNetCore.NodeServices.Tests.csproj", "{F7E4CC45-B553-4D58-8B3E-B9F426FAF67F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HeaderPropagation", "HeaderPropagation", "{5527E368-FD50-4E8C-B8D8-C3D1374BE4F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.HeaderPropagation", "src\Middleware\HeaderPropagation\src\Microsoft.AspNetCore.HeaderPropagation.csproj", "{EC7CA990-BB0E-44AF-81B6-44E0E27FDE9B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HeaderPropagation", "src\Middleware\HeaderPropagation\src\Microsoft.AspNetCore.HeaderPropagation.csproj", "{EC7CA990-BB0E-44AF-81B6-44E0E27FDE9B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.HeaderPropagation.Tests", "src\Middleware\HeaderPropagation\test\Microsoft.AspNetCore.HeaderPropagation.Tests.csproj", "{399AC9FB-7DCA-4868-B299-2EE4C88D41AD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HeaderPropagation.Tests", "src\Middleware\HeaderPropagation\test\Microsoft.AspNetCore.HeaderPropagation.Tests.csproj", "{399AC9FB-7DCA-4868-B299-2EE4C88D41AD}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C2FDF6AA-A8BD-40A6-81AD-14687927E9FC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeaderPropagationSample", "src\Middleware\HeaderPropagation\samples\HeaderPropagationSample\HeaderPropagationSample.csproj", "{91BD5675-6674-4053-AD04-19F28DD3BAE5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HeaderPropagationSample", "src\Middleware\HeaderPropagation\samples\HeaderPropagationSample\HeaderPropagationSample.csproj", "{91BD5675-6674-4053-AD04-19F28DD3BAE5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConcurrencyLimiter", "ConcurrencyLimiter", "{4F8ED87D-E4D9-4941-94D7-D529D83DDBA0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConcurrencyLimiterSample", "src\Middleware\ConcurrencyLimiter\sample\ConcurrencyLimiterSample.csproj", "{56BA4AB2-B915-46DC-AE67-234D60BFEDBC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConcurrencyLimiterSample", "src\Middleware\ConcurrencyLimiter\sample\ConcurrencyLimiterSample.csproj", "{56BA4AB2-B915-46DC-AE67-234D60BFEDBC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ConcurrencyLimiter", "src\Middleware\ConcurrencyLimiter\src\Microsoft.AspNetCore.ConcurrencyLimiter.csproj", "{532D447F-0CDE-4BA6-B181-DD1FB4B07BF0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ConcurrencyLimiter", "src\Middleware\ConcurrencyLimiter\src\Microsoft.AspNetCore.ConcurrencyLimiter.csproj", "{532D447F-0CDE-4BA6-B181-DD1FB4B07BF0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ConcurrencyLimiter.Tests", "src\Middleware\ConcurrencyLimiter\test\Microsoft.AspNetCore.ConcurrencyLimiter.Tests.csproj", "{2D6567CC-2BAE-4208-BBD0-F8D2F6E57154}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ConcurrencyLimiter.Tests", "src\Middleware\ConcurrencyLimiter\test\Microsoft.AspNetCore.ConcurrencyLimiter.Tests.csproj", "{2D6567CC-2BAE-4208-BBD0-F8D2F6E57154}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{9ECF118E-D7A5-4805-B698-DE9013BB91C6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ConcurrencyLimiter.Microbenchmarks", "src\Middleware\ConcurrencyLimiter\perf\Microbenchmarks\Microsoft.AspNetCore.ConcurrencyLimiter.Microbenchmarks.csproj", "{5BF572A5-24AF-4815-BF0C-F57DA650207D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ConcurrencyLimiter.Microbenchmarks", "src\Middleware\ConcurrencyLimiter\perf\Microbenchmarks\Microsoft.AspNetCore.ConcurrencyLimiter.Microbenchmarks.csproj", "{5BF572A5-24AF-4815-BF0C-F57DA650207D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IntegrationTesting.IIS", "IntegrationTesting.IIS", "{036FB9FC-7F26-4982-B94E-2C32B4C836E1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Server.IntegrationTesting.IIS", "src\Servers\IIS\IntegrationTesting.IIS\src\Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj", "{559F1CCF-7E01-4E27-AB45-2E3B6B4984E1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IntegrationTesting.IIS", "src\Servers\IIS\IntegrationTesting.IIS\src\Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj", "{559F1CCF-7E01-4E27-AB45-2E3B6B4984E1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SpaServices.Extensions.Tests", "src\Middleware\SpaServices.Extensions\test\Microsoft.AspNetCore.SpaServices.Extensions.Tests.csproj", "{AF964703-404B-4632-9D1F-8EEE646BBA37}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaServices.Extensions.Tests", "src\Middleware\SpaServices.Extensions\test\Microsoft.AspNetCore.SpaServices.Extensions.Tests.csproj", "{AF964703-404B-4632-9D1F-8EEE646BBA37}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{EE65018D-FA12-461D-B2C5-44CA6E385530}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebSockets.Microbenchmarks", "src\Middleware\perf\Microbenchmarks\Microsoft.AspNetCore.WebSockets.Microbenchmarks.csproj", "{A8E1962B-688E-44B3-81F3-BBB9891534CE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.Microbenchmarks", "src\Middleware\perf\Microbenchmarks\Microsoft.AspNetCore.WebSockets.Microbenchmarks.csproj", "{A8E1962B-688E-44B3-81F3-BBB9891534CE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SpaServices.Tests", "src\Middleware\SpaServices\test\Microsoft.AspNetCore.SpaServices.Tests.csproj", "{81E8CF5B-F285-40C6-B935-6E5F7AA7A072}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaServices.Tests", "src\Middleware\SpaServices\test\Microsoft.AspNetCore.SpaServices.Tests.csproj", "{81E8CF5B-F285-40C6-B935-6E5F7AA7A072}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching.Microbenchmarks", "src\Middleware\perf\ResponseCaching.Microbenchmarks\Microsoft.AspNetCore.ResponseCaching.Microbenchmarks.csproj", "{8A745E35-8098-4EB4-AC55-587B9F0DC4BE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Microbenchmarks", "src\Middleware\perf\ResponseCaching.Microbenchmarks\Microsoft.AspNetCore.ResponseCaching.Microbenchmarks.csproj", "{8A745E35-8098-4EB4-AC55-587B9F0DC4BE}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MusicStore", "MusicStore", "{884AED21-7931-42A3-B08A-E58F7B0D6E7F}" EndProject @@ -1441,6 +1441,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestContentPackage", "src\C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components.TestServer", "src\Components\test\testassets\TestServer\Components.TestServer.csproj", "{8A59AF88-4A82-46ED-977D-D909001F8107}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSP", "CSP", "{C0D33362-7737-4A65-92F7-00007F39C41B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp", "src\Middleware\CSP\src\Microsoft.AspNetCore.Csp.csproj", "{CDC01EA8-6718-44C9-B826-BB56DD66CFC9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Csp.Test", "src\Middleware\CSP\test\UnitTests\Microsoft.AspNetCore.Csp.Test.csproj", "{1F11BE90-A5E5-4A6E-A715-59690DBE08D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CspMiddlewareWebSite", "src\Middleware\CSP\test\testassets\CspApplication\CspMiddlewareWebSite.csproj", "{B02D8184-883E-4AB1-ABDD-6BB155B87CA8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -6815,6 +6823,42 @@ Global {8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x64.Build.0 = Release|Any CPU {8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x86.ActiveCfg = Release|Any CPU {8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x86.Build.0 = Release|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Debug|x64.Build.0 = Debug|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Debug|x86.Build.0 = Debug|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Release|Any CPU.Build.0 = Release|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Release|x64.ActiveCfg = Release|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Release|x64.Build.0 = Release|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Release|x86.ActiveCfg = Release|Any CPU + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9}.Release|x86.Build.0 = Release|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Debug|x64.Build.0 = Debug|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Debug|x86.Build.0 = Debug|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Release|Any CPU.Build.0 = Release|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Release|x64.ActiveCfg = Release|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Release|x64.Build.0 = Release|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Release|x86.ActiveCfg = Release|Any CPU + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9}.Release|x86.Build.0 = Release|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Debug|x64.ActiveCfg = Debug|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Debug|x64.Build.0 = Debug|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Debug|x86.ActiveCfg = Debug|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Debug|x86.Build.0 = Debug|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Release|Any CPU.Build.0 = Release|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Release|x64.ActiveCfg = Release|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Release|x64.Build.0 = Release|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Release|x86.ActiveCfg = Release|Any CPU + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -7537,6 +7581,10 @@ Global {ADF9C126-F322-4E34-AFD3-E626A4487206} = {6126DCE4-9692-4EE2-B240-C65743572995} {3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31} = {6126DCE4-9692-4EE2-B240-C65743572995} {8A59AF88-4A82-46ED-977D-D909001F8107} = {6126DCE4-9692-4EE2-B240-C65743572995} + {C0D33362-7737-4A65-92F7-00007F39C41B} = {E5963C9F-20A6-4385-B364-814D2581FADF} + {CDC01EA8-6718-44C9-B826-BB56DD66CFC9} = {C0D33362-7737-4A65-92F7-00007F39C41B} + {1F11BE90-A5E5-4A6E-A715-59690DBE08D9} = {C0D33362-7737-4A65-92F7-00007F39C41B} + {B02D8184-883E-4AB1-ABDD-6BB155B87CA8} = {C0D33362-7737-4A65-92F7-00007F39C41B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} From 535088d3735695038b59336c91e1fdfcb3626d1e Mon Sep 17 00:00:00 2001 From: aaron <5382864+aaronshim@users.noreply.github.com> Date: Fri, 17 Jul 2020 15:39:17 +0000 Subject: [PATCH 14/26] Middleware only adds CSP on text/html responses --- src/Middleware/CSP/src/CspMiddleware.cs | 5 ++- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 75aec9e620d0..488a8a6346dd 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -17,7 +17,10 @@ public CspMiddleware(RequestDelegate next, ContentSecurityPolicy csp) public Task Invoke(HttpContext context) { - context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(); + if (context.Request.ContentType == null || context.Request.ContentType.Equals("text/html")) + { + context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(); + } return _next(context); } } diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 554915a28f30..55031694b014 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -51,6 +51,48 @@ public async Task cspHeaderIsSetOnAllResponses(string requestPath) } } + [Theory] + [InlineData("text/html", true)] + [InlineData("application/json", false)] + [InlineData(null, true)] + public async Task cspHeaderIsSetOnlyOnValidResponses(string contentType, bool headerShouldExist) + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING); + }); + app.Run(async context => + { + await context.Response.WriteAsync("Test response"); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + var response = await server.CreateRequest("/").AddHeader("content-type", contentType) + .SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + if (headerShouldExist) + { + Assert.Single(response.Headers); + Assert.NotEmpty(response.Headers.GetValues(CspConstants.CspEnforcedHeaderName).FirstOrDefault()); + Assert.Equal("Test response", await response.Content.ReadAsStringAsync()); + } + else + { + Assert.Empty(response.Headers); + } + } + } + [Theory] [InlineData("GET", "foo")] [InlineData("GET", "application/csp-report")] From 5b6669239ba23efd5473dab2b815502075aa8df7 Mon Sep 17 00:00:00 2001 From: aaron <5382864+aaronshim@users.noreply.github.com> Date: Fri, 17 Jul 2020 15:39:44 +0000 Subject: [PATCH 15/26] Changing the order of middleware so that pages that use routing still have CSP --- .../CspApplication/Pages/Shared/_Layout.cshtml | 1 - .../CSP/test/testassets/CspApplication/Startup.cs | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml index 1588a79c8564..0f584c3ce124 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml @@ -4,7 +4,6 @@ @ViewData["Title"] - CspApplication - diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs index b39fdc21204d..d62b1e6ddc8f 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs +++ b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -29,6 +30,10 @@ public void ConfigureServices(IServiceCollection services) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + // CSP configuration. Must come first because other middleware might skip any following middleware. + app.UseCsp(policyBuilder => policyBuilder.WithCspMode(CspMode.REPORTING) + .WithReportingUri("/csp")); + // Not sure how many of these we absolutely need to do a basic templated HTML page with a reporting endpoint. app.UseDeveloperExceptionPage(); app.UseStaticFiles(); @@ -38,15 +43,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); }); - - // CSP configuration. - app.UseCsp(policyBuilder => policyBuilder.WithCspMode(CspMode.REPORTING) - .WithReportingUri("/csp")); } public static void Main(string[] args) { var host = new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) .UseKestrel() .UseIISIntegration() .UseStartup() From 52d9e94669d0d1b48dffaa3a6379d7d9c4edb422 Mon Sep 17 00:00:00 2001 From: aaron <5382864+aaronshim@users.noreply.github.com> Date: Fri, 17 Jul 2020 15:58:44 +0000 Subject: [PATCH 16/26] Null check during CSP report logging --- src/Middleware/CSP/src/CspReportingMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/CSP/src/CspReportingMiddleware.cs b/src/Middleware/CSP/src/CspReportingMiddleware.cs index 377fd6a126f4..a5e0d94c97f3 100644 --- a/src/Middleware/CSP/src/CspReportingMiddleware.cs +++ b/src/Middleware/CSP/src/CspReportingMiddleware.cs @@ -35,7 +35,7 @@ private bool IsReportRequest(HttpRequest request) { // TODO: Is this first condition guaranteed? return request.Path.StartsWithSegments(_loggingConfig.ReportUri) - && request.ContentType.StartsWith(CspConstants.CspReportContentType) + && request.ContentType?.StartsWith(CspConstants.CspReportContentType) == true && request.ContentLength != 0; } From c585d1fafb4004210909d5f059e2d33c15221b8a Mon Sep 17 00:00:00 2001 From: aaron <5382864+aaronshim@users.noreply.github.com> Date: Fri, 17 Jul 2020 22:45:03 +0000 Subject: [PATCH 17/26] Initial implementation/scaffolding for TagHelpers to auto-nonce script tags. ASP.NET doesn't seem to allow Optional/Nullable binds, so we will have to call AddNonces on every webapp that uses CSP. --- .../CSP/src/ContentSecurityPolicy.cs | 5 ++- src/Middleware/CSP/src/CspMiddleware.cs | 4 +- .../CSP/src/CspMiddlewareExtensions.cs | 13 ++++++ src/Middleware/CSP/src/INonce.cs | 27 ++++++++++++ .../CSP/src/Microsoft.AspNetCore.Csp.csproj | 1 + .../CSP/src/NoncedScriptTagHelper.cs | 23 ++++++++++ .../UnitTests/ContentSecurityPolicyTest.cs | 30 +++++++++++++ .../CSP/test/UnitTests/CspMiddlewareTests.cs | 43 ++++++++++++++++++- .../CspApplication/Pages/_ViewImports.cshtml | 3 +- .../test/testassets/CspApplication/Startup.cs | 2 + .../CspApplication/wwwroot/js/site.js | 2 +- 11 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 src/Middleware/CSP/src/INonce.cs create mode 100644 src/Middleware/CSP/src/NoncedScriptTagHelper.cs diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index 8d17fa7cf66c..fb2c9f6b9458 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -39,10 +39,11 @@ public string GetHeaderName() { return _cspMode == CspMode.REPORTING ? CspConstants.CspReportingHeaderName : CspConstants.CspEnforcedHeaderName; } - public string GetPolicy() + public string GetPolicy(INonce nonce=null) { return string.Format( - "script-src 'nonce-random' {0} {1} https: http:; {2}; {3}", + "script-src {0} {1} {2} https: http:; {3}; {4}", + nonce == null ? "" : string.Format("'nonce-{0}'", nonce.GetValue()), _strictDynamic ? "'strict-dynamic'" : "", _unsafeEval ? "'unsafe-eval'" : "", _baseAndObject, diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 488a8a6346dd..447016aadf37 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -15,11 +15,11 @@ public CspMiddleware(RequestDelegate next, ContentSecurityPolicy csp) _csp = csp; } - public Task Invoke(HttpContext context) + public Task Invoke(HttpContext context, INonce nonce) { if (context.Request.ContentType == null || context.Request.ContentType.Equals("text/html")) { - context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(); + context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(nonce); } return _next(context); } diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index 66ec3af04997..a697ec7e6401 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Csp { @@ -26,5 +27,17 @@ public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action(policyBuilder.Build()); } + + public static IServiceCollection AddNonces(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddScoped(); + + return services; + } } } diff --git a/src/Middleware/CSP/src/INonce.cs b/src/Middleware/CSP/src/INonce.cs new file mode 100644 index 000000000000..86a1076de2fc --- /dev/null +++ b/src/Middleware/CSP/src/INonce.cs @@ -0,0 +1,27 @@ +using System; + +namespace Microsoft.AspNetCore.Csp +{ + public interface INonce + { + string GetValue(); + } + + public class Nonce : INonce + { + private readonly string _value; + // TODO: Make sure we use an actually crypto-safe random generator. + private static readonly Lazy _gen = new Lazy(() => new Random()); + + public Nonce() + { + // TODO: Actually come up with an alphanumeric string of enough entropy rather than casting a random int to string. + _value = _gen.Value.Next().ToString(); + } + + public string GetValue() + { + return _value; + } + } +} diff --git a/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj b/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj index 19c6ddd81aec..6f7a0a4a03c0 100644 --- a/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj +++ b/src/Middleware/CSP/src/Microsoft.AspNetCore.Csp.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Middleware/CSP/src/NoncedScriptTagHelper.cs b/src/Middleware/CSP/src/NoncedScriptTagHelper.cs new file mode 100644 index 000000000000..4611cd8e0739 --- /dev/null +++ b/src/Middleware/CSP/src/NoncedScriptTagHelper.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Microsoft.AspNetCore.Csp +{ + [HtmlTargetElement("script")] + public class NoncedScriptTagHelper : TagHelper + { + private readonly INonce _nonce; + + public NoncedScriptTagHelper(INonce nonce) + { + _nonce = nonce; + } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + //base.Process(context, output); + output.TagName = "script"; + output.Attributes.SetAttribute("debug-nonce", _nonce.GetValue()); + output.Attributes.SetAttribute("nonce", _nonce.GetValue()); + } + } +} diff --git a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs index 21beade61768..4a4181fada72 100644 --- a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs +++ b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs @@ -4,6 +4,24 @@ namespace Microsoft.AspNetCore.Csp.Test { public class ContentSecurityPolicyTest { + /// + /// Mocking a INonce for generating policies with a testable, fixed nonce. + /// + private class TestNonce : INonce + { + private readonly string _val; + + public TestNonce(string value) + { + _val = value; + } + + public string GetValue() + { + return _val; + } + } + [Fact] public void SetsCorrectHeaderNameInReportingMode() { @@ -98,5 +116,17 @@ public void AlwaysSetsFallbackHttpAndHttpsProtocolsInScriptSrc() Assert.Matches("script-src .* https: http:", policy.GetPolicy()); } + + [Theory] + [InlineData("ABCDE")] + [InlineData("1234567890")] + public void SetNonceIfProvided(string nonce) + { + var policy = new ContentSecurityPolicyBuilder() + .WithCspMode(CspMode.ENFORCING) + .Build(); + + Assert.Matches(string.Format("'nonce-{0}", nonce), policy.GetPolicy(new TestNonce(nonce))); + } } } diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 55031694b014..84745ccf3ecf 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -20,10 +20,11 @@ public class CspMiddlewareTests [InlineData("/")] [InlineData("/cheese")] [InlineData("/foo")] - public async Task cspHeaderIsSetOnAllResponses(string requestPath) + public async Task CspHeaderIsSetOnAllResponses(string requestPath) { // Arrange var hostBuilder = new WebHostBuilder() + .ConfigureServices(services => services.AddNonces()) .Configure(app => { app.UseCsp(policyBuilder => @@ -55,10 +56,11 @@ public async Task cspHeaderIsSetOnAllResponses(string requestPath) [InlineData("text/html", true)] [InlineData("application/json", false)] [InlineData(null, true)] - public async Task cspHeaderIsSetOnlyOnValidResponses(string contentType, bool headerShouldExist) + public async Task CspHeaderIsSetOnlyOnValidResponses(string contentType, bool headerShouldExist) { // Arrange var hostBuilder = new WebHostBuilder() + .ConfigureServices(services => services.AddNonces()) .Configure(app => { app.UseCsp(policyBuilder => @@ -93,6 +95,43 @@ public async Task cspHeaderIsSetOnlyOnValidResponses(string contentType, bool he } } + [Theory] + [InlineData("/")] + public async Task CspNonceExistsInHeader(string requestPath) + { + // Arrange + var hostBuilder = new WebHostBuilder() + .ConfigureServices(services => services.AddNonces()) + .Configure(app => + { + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING); + }); + app.Run(async context => + { + await context.Response.WriteAsync("Test response"); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + var response = await server.CreateRequest(requestPath) + .SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal("Test response", await response.Content.ReadAsStringAsync()); + + Assert.Single(response.Headers); + var header = response.Headers.GetValues(CspConstants.CspEnforcedHeaderName).FirstOrDefault(); + Assert.NotEmpty(header); + Assert.Matches("'nonce-", header); + } + } + [Theory] [InlineData("GET", "foo")] [InlineData("GET", "application/csp-report")] diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml index 05729b9555e6..3c712194d19b 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/_ViewImports.cshtml @@ -1,3 +1,4 @@ -@using CspApplication +@using CspApplication @namespace CspApplication.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper Microsoft.AspNetCore.Csp.NoncedScriptTagHelper, Microsoft.AspNetCore.Csp diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs index d62b1e6ddc8f..a03d1b81da5c 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs +++ b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs @@ -24,6 +24,7 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddNonces(); services.AddRazorPages(); } @@ -34,6 +35,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseCsp(policyBuilder => policyBuilder.WithCspMode(CspMode.REPORTING) .WithReportingUri("/csp")); + // Not sure how many of these we absolutely need to do a basic templated HTML page with a reporting endpoint. app.UseDeveloperExceptionPage(); app.UseStaticFiles(); diff --git a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js index 5a93640fd6ca..580cf0f85fd6 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js +++ b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js @@ -3,4 +3,4 @@ // Write your Javascript code. -alert('hi'); +alert("I'm a link-loaded script!"); From 511f74bfc78652127d126dfb826da5ff326daa28 Mon Sep 17 00:00:00 2001 From: aaron <5382864+aaronshim@users.noreply.github.com> Date: Sat, 18 Jul 2020 00:43:58 +0000 Subject: [PATCH 18/26] Attempt at scaffolding an integration test to check that the templates have nonces. Does not build yet because of some root directory configuration in the Startup. --- .../CSP/test/UnitTests/CspIntegrationTests.cs | 35 +++++++++++++++++++ .../Microsoft.AspNetCore.Csp.Test.csproj | 12 +++++++ .../CspMiddlewareWebSite.csproj | 1 + .../test/testassets/CspApplication/Startup.cs | 13 +++---- 4 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs diff --git a/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs b/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs new file mode 100644 index 000000000000..c7e0a41ffbf1 --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs @@ -0,0 +1,35 @@ +using Xunit; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Testing; +using CspApplication; + +namespace Microsoft.AspNetCore.Csp.Test +{ + public class CspIntegrationTests : IClassFixture> + { + + private readonly WebApplicationFactory _factory; + + public CspIntegrationTests(WebApplicationFactory factory) + { + _factory = factory; + } + + [Theory] + [InlineData("/")] + public async Task CspNonceAddedToScriptTags(string requestPath) + { + // Arrange + var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync(requestPath); + + // Assert + response.EnsureSuccessStatusCode(); + //var header = response.Headers.GetValues(CspConstants.CspEnforcedHeaderName).FirstOrDefault(); + //Assert.NotEmpty(header); + //Assert.Matches("'nonce-", header); + } + } +} diff --git a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj index c7347d5a2511..d400d1ae1936 100644 --- a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj +++ b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj @@ -2,10 +2,22 @@ $(DefaultNetCoreTargetFramework) + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj b/src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj index 9fa75eeb3355..de383e262222 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj +++ b/src/Middleware/CSP/test/testassets/CspApplication/CspMiddlewareWebSite.csproj @@ -2,6 +2,7 @@ $(DefaultNetCoreTargetFramework) + true diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs index a03d1b81da5c..56b005910190 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs +++ b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs @@ -47,15 +47,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) }); } - public static void Main(string[] args) + public static IWebHostBuilder CreateWebHostBuilder(string[] args) { - var host = new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) + return new WebHostBuilder() .UseKestrel() - .UseIISIntegration() - .UseStartup() - .Build(); + .UseIISIntegration(); + } + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args).UseContentRoot(Directory.GetCurrentDirectory()).UseStartup().Build(); host.Run(); } } From a7e4a7a6372444de4c1999f4223f30c2ca5db426 Mon Sep 17 00:00:00 2001 From: Sal Date: Mon, 20 Jul 2020 10:09:38 +0000 Subject: [PATCH 19/26] Compute CSP up front. Only set up reporting endpoint if report URI is local --- src/Middleware/CSP/CSP.slnf | 7 +- .../CSP/src/ContentSecurityPolicy.cs | 24 ++- .../CSP/src/ContentSecurityPolicyBuilder.cs | 4 +- .../CSP/src/CspMiddlewareExtensions.cs | 5 +- .../UnitTests/ContentSecurityPolicyTest.cs | 22 ++- .../CSP/test/UnitTests/CspIntegrationTests.cs | 3 + .../CSP/test/UnitTests/CspMiddlewareTests.cs | 60 ++++++- .../test/testassets/CspApplication/Startup.cs | 2 +- src/Middleware/Middleware.slnf | 161 ++++++++---------- 9 files changed, 175 insertions(+), 113 deletions(-) diff --git a/src/Middleware/CSP/CSP.slnf b/src/Middleware/CSP/CSP.slnf index 723739ea61da..d559cbaeaae6 100644 --- a/src/Middleware/CSP/CSP.slnf +++ b/src/Middleware/CSP/CSP.slnf @@ -1,9 +1,10 @@ { "solution": { - "path": "D:\\work\\aspnetcore\\src\\Middleware\\Middleware.sln", + "path": "..\\..\\..\\AspNetCore.sln", "projects": [ - "CSP\\src\\Microsoft.AspNetCore.Csp.csproj", - "CSP\\test\\UnitTests\\Microsoft.AspNetCore.Csp.Test.csproj", + "src\\Middleware\\CSP\\src\\Microsoft.AspNetCore.Csp.csproj", + "src\\Middleware\\CSP\\test\\UnitTests\\Microsoft.AspNetCore.Csp.Test.csproj", + "src\\Middleware\\CSP\\test\\testassets\\CspMiddlewareWebSite.csproj", ] } } \ No newline at end of file diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index fb2c9f6b9458..070697b45a9f 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -16,6 +16,7 @@ public enum CspMode public class ContentSecurityPolicy { private readonly string _baseAndObject = "base-uri 'none'; object-src 'none'"; + private readonly Func policyBuilder; private readonly CspMode _cspMode; private readonly bool _strictDynamic; @@ -33,21 +34,28 @@ string reportingUri _strictDynamic = strictDynamic; _unsafeEval = unsafeEval; _reportingUri = reportingUri; + + var policyFormat = new StringBuilder() + .Append("script-src") + .Append(" 'nonce-{0}' ") // nonce + .Append(_strictDynamic ? "'strict-dynamic'" : "") + .Append(_unsafeEval ? "'unsafe-eval'" : "") + .Append(" https: http:;") // fall-back allowlist-based CSP for browsers that don't support nonces + .Append(_baseAndObject) + .Append(";") // end of script-src + .Append(_reportingUri != null ? "report-uri " + _reportingUri : "") + .ToString(); + + policyBuilder = nonce => string.Format(policyFormat, nonce.GetValue()); } public string GetHeaderName() { return _cspMode == CspMode.REPORTING ? CspConstants.CspReportingHeaderName : CspConstants.CspEnforcedHeaderName; } - public string GetPolicy(INonce nonce=null) + public string GetPolicy(INonce nonce) { - return string.Format( - "script-src {0} {1} {2} https: http:; {3}; {4}", - nonce == null ? "" : string.Format("'nonce-{0}'", nonce.GetValue()), - _strictDynamic ? "'strict-dynamic'" : "", - _unsafeEval ? "'unsafe-eval'" : "", - _baseAndObject, - _reportingUri != null ? "report-uri " + _reportingUri : ""); + return policyBuilder.Invoke(nonce); } } } diff --git a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs index 0fe0ca0beeff..04cdfad31c63 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs @@ -41,9 +41,9 @@ public ContentSecurityPolicyBuilder WithLogLevel(LogLevel logLevel) return this; } - public bool HasReporting() + public bool HasLocalReporting() { - return _reportingUri != null; + return _reportingUri != null && _reportingUri.StartsWith("/"); } public LoggingConfiguration LoggingConfiguration() diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index a697ec7e6401..36276d77b003 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -16,8 +16,7 @@ public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action(policyBuilder.Build()); } - public static IServiceCollection AddNonces(this IServiceCollection services) + public static IServiceCollection AddCsp(this IServiceCollection services) { if (services == null) { diff --git a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs index 4a4181fada72..59a0f6a561b4 100644 --- a/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs +++ b/src/Middleware/CSP/test/UnitTests/ContentSecurityPolicyTest.cs @@ -50,7 +50,7 @@ public void WhenStrictDynamicNotSet_BuildsPolicyCorrectly() .WithCspMode(CspMode.ENFORCING) .Build(); - Assert.DoesNotContain("strict-dynamic", policy.GetPolicy()); + Assert.DoesNotContain("strict-dynamic", policy.GetPolicy(new InertNonce())); } [Fact] @@ -61,7 +61,7 @@ public void WhenStrictDynamicSet_BuildsPolicyCorrectly() .WithStrictDynamic() .Build(); - Assert.Contains("strict-dynamic", policy.GetPolicy()); + Assert.Contains("strict-dynamic", policy.GetPolicy(new InertNonce())); } [Fact] @@ -71,7 +71,7 @@ public void WhenUnsafeEvalNotSet_BuildsPolicyCorrectly() .WithCspMode(CspMode.ENFORCING) .Build(); - Assert.DoesNotContain("unsafe-eval", policy.GetPolicy()); + Assert.DoesNotContain("unsafe-eval", policy.GetPolicy(new InertNonce())); } [Fact] @@ -82,7 +82,7 @@ public void WhenUnsafeEvalSet_BuildsPolicyCorrectly() .WithUnsafeEval() .Build(); - Assert.Contains("unsafe-eval", policy.GetPolicy()); + Assert.Contains("unsafe-eval", policy.GetPolicy(new InertNonce())); } // TODO: Add more coverage around reporting URLs @@ -94,7 +94,7 @@ public void WhenReportingUriSet_BuildsPolicyCorrectly() .WithReportingUri("/cspreport") .Build(); - Assert.Contains("report-uri /cspreport", policy.GetPolicy()); + Assert.Contains("report-uri /cspreport", policy.GetPolicy(new InertNonce())); } [Fact] @@ -104,7 +104,7 @@ public void AlwaysRestrictsBaseUriAndObjectSrcToNone() .WithCspMode(CspMode.ENFORCING) .Build(); - Assert.Contains("base-uri 'none'; object-src 'none';", policy.GetPolicy()); + Assert.Contains("base-uri 'none'; object-src 'none';", policy.GetPolicy(new InertNonce())); } [Fact] @@ -114,7 +114,7 @@ public void AlwaysSetsFallbackHttpAndHttpsProtocolsInScriptSrc() .WithCspMode(CspMode.ENFORCING) .Build(); - Assert.Matches("script-src .* https: http:", policy.GetPolicy()); + Assert.Matches("script-src .* https: http:", policy.GetPolicy(new InertNonce())); } [Theory] @@ -129,4 +129,12 @@ public void SetNonceIfProvided(string nonce) Assert.Matches(string.Format("'nonce-{0}", nonce), policy.GetPolicy(new TestNonce(nonce))); } } + + public class InertNonce : INonce + { + public string GetValue() + { + return "inert"; + } + } } diff --git a/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs b/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs index c7e0a41ffbf1..40aecdfe947e 100644 --- a/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs @@ -2,6 +2,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Testing; using CspApplication; +using Microsoft.AspNetCore.TestHost; +using System.IO; +using Microsoft.AspNetCore.Hosting; namespace Microsoft.AspNetCore.Csp.Test { diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 84745ccf3ecf..54dca237caad 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -24,7 +24,7 @@ public async Task CspHeaderIsSetOnAllResponses(string requestPath) { // Arrange var hostBuilder = new WebHostBuilder() - .ConfigureServices(services => services.AddNonces()) + .ConfigureServices(services => services.AddCsp()) .Configure(app => { app.UseCsp(policyBuilder => @@ -60,7 +60,7 @@ public async Task CspHeaderIsSetOnlyOnValidResponses(string contentType, bool he { // Arrange var hostBuilder = new WebHostBuilder() - .ConfigureServices(services => services.AddNonces()) + .ConfigureServices(services => services.AddCsp()) .Configure(app => { app.UseCsp(policyBuilder => @@ -101,7 +101,7 @@ public async Task CspNonceExistsInHeader(string requestPath) { // Arrange var hostBuilder = new WebHostBuilder() - .ConfigureServices(services => services.AddNonces()) + .ConfigureServices(services => services.AddCsp()) .Configure(app => { app.UseCsp(policyBuilder => @@ -230,5 +230,59 @@ public async void ProcessesCspReportRequestsCorrectly() //logger.Verify(m => m.Log(LogLevel.Information, "")); } } + + [Fact] + public async void DoesNotCollectReportsIfReportingUriIsNotRelative() + { + // Arrange + var logger = new Mock>(); + + var hostBuilder = new WebHostBuilder() + .ConfigureServices(services => services.AddCsp()) + .Configure(app => + { + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING) + .WithReportingUri("https://cheese.com/cspreport"); + }); + app.Run(async context => + { + await context.Response.WriteAsync("Test response"); + }); + }) + .ConfigureServices(services => + { + services.AddSingleton(typeof(ILogger), logger.Object); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + var context = await server.SendAsync(c => + { + c.Request.Method = "POST"; + c.Request.Path = "/cspreport"; + c.Request.Headers[HeaderNames.ContentType] = CspConstants.CspReportContentType; + c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes( + @"{ + ""csp-report"": { + ""document-uri"": ""http://example.com/signup.html"", + ""referrer"": ""http://evil.com"", + ""blocked-uri"": ""http://example.com/css/style.css"", + ""violated-directive"": ""style-src cdn.example.com"", + ""original-policy"": ""default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports"", + ""disposition"": ""report"" + } + }" + )); + }); + + // Assert + Assert.NotEqual(204, context.Response.StatusCode); + Assert.NotEmpty(new StreamReader(context.Response.Body, Encoding.UTF8).ReadToEnd()); + } + } } } diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs index 56b005910190..04afdc741dd9 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs +++ b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs @@ -24,7 +24,7 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddNonces(); + services.AddCsp(); services.AddRazorPages(); } diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index b2feb33f9548..4b4683b69b64 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -1,33 +1,47 @@ -{ +{ "solution": { "path": "..\\..\\AspNetCore.sln", - "projects" : [ - "src\\Middleware\\WebSockets\\samples\\EchoApp\\EchoApp.csproj", - "src\\Middleware\\WebSockets\\samples\\TestServer\\WebSockets.TestServer.csproj", - "src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj", - "src\\Middleware\\WebSockets\\test\\ConformanceTests\\Microsoft.AspNetCore.WebSockets.ConformanceTests.csproj", - "src\\Middleware\\WebSockets\\test\\UnitTests\\Microsoft.AspNetCore.WebSockets.Tests.csproj", - "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", - "src\\Middleware\\Diagnostics\\test\\FunctionalTests\\Diagnostics.FunctionalTests.csproj", - "src\\Middleware\\Diagnostics\\test\\testassets\\ClassLibraryWithPortablePdbs\\ClassLibraryWithPortablePdbs.csproj", - "src\\Middleware\\Diagnostics\\test\\testassets\\DatabaseErrorPageSample\\DatabaseErrorPageSample.csproj", - "src\\Middleware\\Diagnostics\\test\\testassets\\DeveloperExceptionPageSample\\DeveloperExceptionPageSample.csproj", - "src\\Middleware\\Diagnostics\\test\\testassets\\ExceptionHandlerSample\\ExceptionHandlerSample.csproj", - "src\\Middleware\\Diagnostics\\test\\testassets\\StatusCodePagesSample\\StatusCodePagesSample.csproj", - "src\\Middleware\\Diagnostics\\test\\testassets\\WelcomePageSample\\WelcomePageSample.csproj", - "src\\Middleware\\Diagnostics\\test\\UnitTests\\Microsoft.AspNetCore.Diagnostics.Tests.csproj", + "projects": [ + "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj", + "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj", + "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj", + "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", + "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", + "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", + "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", + "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", + "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj", + "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", + "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", + "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", + "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", + "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", + "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", + "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", + "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", + "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", + "src\\Middleware\\CORS\\test\\UnitTests\\Microsoft.AspNetCore.Cors.Test.csproj", + "src\\Middleware\\CSP\\src\\Microsoft.AspNetCore.Csp.csproj", + "src\\Middleware\\CSP\\test\\UnitTests\\Microsoft.AspNetCore.Csp.Test.csproj", + "src\\Middleware\\CSP\\test\\testassets\\CspApplication\\CspMiddlewareWebSite.csproj", + "src\\Middleware\\ConcurrencyLimiter\\perf\\Microbenchmarks\\Microsoft.AspNetCore.ConcurrencyLimiter.Microbenchmarks.csproj", + "src\\Middleware\\ConcurrencyLimiter\\sample\\ConcurrencyLimiterSample.csproj", + "src\\Middleware\\ConcurrencyLimiter\\src\\Microsoft.AspNetCore.ConcurrencyLimiter.csproj", + "src\\Middleware\\ConcurrencyLimiter\\test\\Microsoft.AspNetCore.ConcurrencyLimiter.Tests.csproj", "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", "src\\Middleware\\Diagnostics.EntityFrameworkCore\\src\\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj", "src\\Middleware\\Diagnostics.EntityFrameworkCore\\test\\FunctionalTests\\Diagnostics.EFCore.FunctionalTests.csproj", "src\\Middleware\\Diagnostics.EntityFrameworkCore\\test\\UnitTests\\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.csproj", - "src\\Middleware\\MiddlewareAnalysis\\samples\\MiddlewareAnalysisSample\\MiddlewareAnalysisSample.csproj", - "src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", - "src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj", - "src\\Middleware\\HealthChecks\\src\\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj", - "src\\Middleware\\HealthChecks\\test\\testassets\\HealthChecksSample\\HealthChecksSample.csproj", - "src\\Middleware\\HealthChecks\\test\\UnitTests\\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj", + "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", + "src\\Middleware\\Diagnostics\\test\\FunctionalTests\\Diagnostics.FunctionalTests.csproj", + "src\\Middleware\\Diagnostics\\test\\UnitTests\\Microsoft.AspNetCore.Diagnostics.Tests.csproj", + "src\\Middleware\\HeaderPropagation\\samples\\HeaderPropagationSample\\HeaderPropagationSample.csproj", + "src\\Middleware\\HeaderPropagation\\src\\Microsoft.AspNetCore.HeaderPropagation.csproj", + "src\\Middleware\\HeaderPropagation\\test\\Microsoft.AspNetCore.HeaderPropagation.Tests.csproj", "src\\Middleware\\HealthChecks.EntityFrameworkCore\\src\\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj", "src\\Middleware\\HealthChecks.EntityFrameworkCore\\test\\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj", + "src\\Middleware\\HealthChecks\\src\\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj", + "src\\Middleware\\HealthChecks\\test\\UnitTests\\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj", "src\\Middleware\\HostFiltering\\sample\\HostFilteringSample.csproj", "src\\Middleware\\HostFiltering\\src\\Microsoft.AspNetCore.HostFiltering.csproj", "src\\Middleware\\HostFiltering\\test\\Microsoft.AspNetCore.HostFiltering.Tests.csproj", @@ -37,82 +51,57 @@ "src\\Middleware\\HttpsPolicy\\sample\\HttpsPolicySample.csproj", "src\\Middleware\\HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", "src\\Middleware\\HttpsPolicy\\test\\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", - "src\\Middleware\\ResponseCompression\\sample\\ResponseCompressionSample.csproj", - "src\\Middleware\\ResponseCompression\\src\\Microsoft.AspNetCore.ResponseCompression.csproj", - "src\\Middleware\\ResponseCompression\\test\\Microsoft.AspNetCore.ResponseCompression.Tests.csproj", - "src\\Middleware\\Rewrite\\sample\\RewriteSample.csproj", - "src\\Middleware\\Rewrite\\src\\Microsoft.AspNetCore.Rewrite.csproj", - "src\\Middleware\\Rewrite\\test\\Microsoft.AspNetCore.Rewrite.Tests.csproj", + "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", + "src\\Middleware\\Localization.Routing\\test\\Microsoft.AspNetCore.Localization.Routing.Tests.csproj", "src\\Middleware\\Localization\\sample\\LocalizationSample.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", "src\\Middleware\\Localization\\test\\FunctionalTests\\Microsoft.AspNetCore.Localization.FunctionalTests.csproj", "src\\Middleware\\Localization\\test\\UnitTests\\Microsoft.AspNetCore.Localization.Tests.csproj", - "src\\Middleware\\Localization\\testassets\\LocalizationWebsite\\LocalizationWebsite.csproj", - "src\\Middleware\\Localization\\testassets\\ResourcesClassLibraryNoAttribute\\ResourcesClassLibraryNoAttribute.csproj", - "src\\Middleware\\Localization\\testassets\\ResourcesClassLibraryWithAttribute\\ResourcesClassLibraryWithAttribute.csproj", - "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", - "src\\Middleware\\Localization.Routing\\test\\Microsoft.AspNetCore.Localization.Routing.Tests.csproj", - "src\\Middleware\\ResponseCompression\\perf\\Microsoft.AspNetCore.ResponseCompression.Performance.csproj", - "src\\Middleware\\WebSockets\\test\\ConformanceTests\\AutobahnTestApp\\AutobahnTestApp.csproj", - "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", - "src\\Middleware\\CORS\\test\\UnitTests\\Microsoft.AspNetCore.Cors.Test.csproj", - "src\\Middleware\\CORS\\test\\testassets\\CorsMiddlewareWebSite\\CorsMiddlewareWebSite.csproj", - "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", - "src\\Middleware\\StaticFiles\\samples\\StaticFileSample\\StaticFileSample.csproj", - "src\\Middleware\\StaticFiles\\test\\FunctionalTests\\Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj", - "src\\Middleware\\StaticFiles\\test\\UnitTests\\Microsoft.AspNetCore.StaticFiles.Tests.csproj", - "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", + "src\\Middleware\\MiddlewareAnalysis\\samples\\MiddlewareAnalysisSample\\MiddlewareAnalysisSample.csproj", + "src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", + "src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj", + "src\\Middleware\\NodeServices\\samples\\NodeServicesExamples\\NodeServicesExamples.csproj", + "src\\Middleware\\NodeServices\\src\\Microsoft.AspNetCore.NodeServices.csproj", + "src\\Middleware\\NodeServices\\test\\Microsoft.AspNetCore.NodeServices.Tests.csproj", + "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching\\samples\\ResponseCachingSample\\ResponseCachingSample.csproj", + "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", "src\\Middleware\\ResponseCaching\\test\\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", - "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", + "src\\Middleware\\ResponseCompression\\perf\\Microsoft.AspNetCore.ResponseCompression.Performance.csproj", + "src\\Middleware\\ResponseCompression\\sample\\ResponseCompressionSample.csproj", + "src\\Middleware\\ResponseCompression\\src\\Microsoft.AspNetCore.ResponseCompression.csproj", + "src\\Middleware\\ResponseCompression\\test\\Microsoft.AspNetCore.ResponseCompression.Tests.csproj", + "src\\Middleware\\Rewrite\\sample\\RewriteSample.csproj", + "src\\Middleware\\Rewrite\\src\\Microsoft.AspNetCore.Rewrite.csproj", + "src\\Middleware\\Rewrite\\test\\Microsoft.AspNetCore.Rewrite.Tests.csproj", "src\\Middleware\\Session\\samples\\SessionSample.csproj", "src\\Middleware\\Session\\src\\Microsoft.AspNetCore.Session.csproj", "src\\Middleware\\Session\\test\\Microsoft.AspNetCore.Session.Tests.csproj", - "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", - "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj", - "src\\http\\http\\src\\Microsoft.AspNetCore.Http.csproj", - "src\\http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", - "src\\http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", - "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", - "src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj", - "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", - "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", + "src\\Middleware\\SpaServices.Extensions\\src\\Microsoft.AspNetCore.SpaServices.Extensions.csproj", + "src\\Middleware\\SpaServices.Extensions\\test\\Microsoft.AspNetCore.SpaServices.Extensions.Tests.csproj", + "src\\Middleware\\SpaServices\\src\\Microsoft.AspNetCore.SpaServices.csproj", + "src\\Middleware\\SpaServices\\test\\Microsoft.AspNetCore.SpaServices.Tests.csproj", + "src\\Middleware\\StaticFiles\\samples\\StaticFileSample\\StaticFileSample.csproj", + "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", + "src\\Middleware\\StaticFiles\\test\\FunctionalTests\\Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj", + "src\\Middleware\\StaticFiles\\test\\UnitTests\\Microsoft.AspNetCore.StaticFiles.Tests.csproj", + "src\\Middleware\\WebSockets\\samples\\EchoApp\\EchoApp.csproj", + "src\\Middleware\\WebSockets\\samples\\TestServer\\WebSockets.TestServer.csproj", + "src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj", + "src\\Middleware\\WebSockets\\test\\ConformanceTests\\AutobahnTestApp\\AutobahnTestApp.csproj", + "src\\Middleware\\WebSockets\\test\\ConformanceTests\\Microsoft.AspNetCore.WebSockets.ConformanceTests.csproj", + "src\\Middleware\\WebSockets\\test\\UnitTests\\Microsoft.AspNetCore.WebSockets.Tests.csproj", + "src\\Middleware\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebSockets.Microbenchmarks.csproj", + "src\\Middleware\\perf\\ResponseCaching.Microbenchmarks\\Microsoft.AspNetCore.ResponseCaching.Microbenchmarks.csproj", + "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", - "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", - "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", - "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", - "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", - "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", - "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", - "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", - "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", - "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", - "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", - "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj", - "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj", - "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj", + "src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj", "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", - "src\\Middleware\\NodeServices\\samples\\NodeServicesExamples\\NodeServicesExamples.csproj", - "src\\Middleware\\NodeServices\\src\\Microsoft.AspNetCore.NodeServices.csproj", - "src\\Middleware\\SpaServices\\src\\Microsoft.AspNetCore.SpaServices.csproj", - "src\\Middleware\\SpaServices.Extensions\\src\\Microsoft.AspNetCore.SpaServices.Extensions.csproj", - "src\\Middleware\\NodeServices\\test\\Microsoft.AspNetCore.NodeServices.Tests.csproj", - "src\\Middleware\\HeaderPropagation\\src\\Microsoft.AspNetCore.HeaderPropagation.csproj", - "src\\Middleware\\HeaderPropagation\\test\\Microsoft.AspNetCore.HeaderPropagation.Tests.csproj", - "src\\Middleware\\HeaderPropagation\\samples\\HeaderPropagationSample\\HeaderPropagationSample.csproj", "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj", - "src\\Middleware\\ConcurrencyLimiter\\sample\\ConcurrencyLimiterSample.csproj", - "src\\Middleware\\ConcurrencyLimiter\\src\\Microsoft.AspNetCore.ConcurrencyLimiter.csproj", - "src\\Middleware\\ConcurrencyLimiter\\test\\Microsoft.AspNetCore.ConcurrencyLimiter.Tests.csproj", - "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", - "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", - "src\\Middleware\\ConcurrencyLimiter\\perf\\Microbenchmarks\\Microsoft.AspNetCore.ConcurrencyLimiter.Microbenchmarks.csproj", - "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", "src\\Servers\\IIS\\IntegrationTesting.IIS\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj", - "src\\Middleware\\SpaServices.Extensions\\test\\Microsoft.AspNetCore.SpaServices.Extensions.Tests.csproj", - "src\\Middleware\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebSockets.Microbenchmarks.csproj", - "src\\Middleware\\SpaServices\\test\\Microsoft.AspNetCore.SpaServices.Tests.csproj", - "src\\Middleware\\perf\\ResponseCaching.Microbenchmarks\\Microsoft.AspNetCore.ResponseCaching.Microbenchmarks.csproj" + "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", + "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", + "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" ] } -} +} \ No newline at end of file From 2e1f223137b07d7ebcaf34411eb823a219e7c0d2 Mon Sep 17 00:00:00 2001 From: Sal Date: Mon, 20 Jul 2020 16:19:16 +0000 Subject: [PATCH 20/26] Refactor CSP logging and add test --- .../CSP/src/ContentSecurityPolicy.cs | 3 +- .../CSP/src/ContentSecurityPolicyBuilder.cs | 8 +--- .../CSP/src/CspMiddlewareExtensions.cs | 9 ++-- src/Middleware/CSP/src/CspReportLogger.cs | 45 +++++++++++++++++++ .../CSP/src/CspReportingMiddleware.cs | 20 +++------ .../CSP/src/LoggingConfiguration.cs | 11 ----- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 36 +++++++++------ 7 files changed, 83 insertions(+), 49 deletions(-) create mode 100644 src/Middleware/CSP/src/CspReportLogger.cs delete mode 100644 src/Middleware/CSP/src/LoggingConfiguration.cs diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index 070697b45a9f..e38556c363e2 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -35,6 +35,7 @@ string reportingUri _unsafeEval = unsafeEval; _reportingUri = reportingUri; + // compute the static directives of the policy up front to avoid doing so on every request var policyFormat = new StringBuilder() .Append("script-src") .Append(" 'nonce-{0}' ") // nonce @@ -42,7 +43,7 @@ string reportingUri .Append(_unsafeEval ? "'unsafe-eval'" : "") .Append(" https: http:;") // fall-back allowlist-based CSP for browsers that don't support nonces .Append(_baseAndObject) - .Append(";") // end of script-src + .Append("; ") // end of script-src .Append(_reportingUri != null ? "report-uri " + _reportingUri : "") .ToString(); diff --git a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs index 04cdfad31c63..cb9bcfc29bad 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs @@ -46,13 +46,9 @@ public bool HasLocalReporting() return _reportingUri != null && _reportingUri.StartsWith("/"); } - public LoggingConfiguration LoggingConfiguration() + public CspReportLogger ReportLogger(ICspReportLoggerFactory loggerFactory) { - return new LoggingConfiguration - { - LogLevel = _logLevel, - ReportUri = _reportingUri - }; + return loggerFactory.BuildLogger(_logLevel, _reportingUri); } public ContentSecurityPolicy Build() diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index 36276d77b003..7d4431dc6991 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp { @@ -18,10 +19,11 @@ public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action(); + var reportLogger = policyBuilder.ReportLogger(loggerFactory); app.UseWhen( - context => context.Request.Path.StartsWithSegments(loggingConfig.ReportUri), - appBuilder => appBuilder.UseMiddleware(loggingConfig)); + context => context.Request.Path.StartsWithSegments(reportLogger.ReportUri), + appBuilder => appBuilder.UseMiddleware(reportLogger)); } return app.UseMiddleware(policyBuilder.Build()); @@ -35,6 +37,7 @@ public static IServiceCollection AddCsp(this IServiceCollection services) } services.AddScoped(); + services.AddSingleton(); return services; } diff --git a/src/Middleware/CSP/src/CspReportLogger.cs b/src/Middleware/CSP/src/CspReportLogger.cs new file mode 100644 index 000000000000..f52798e7b7f1 --- /dev/null +++ b/src/Middleware/CSP/src/CspReportLogger.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Csp +{ + public interface ICspReportLoggerFactory + { + CspReportLogger BuildLogger(LogLevel logLevel, string reportUri); + } + + public class CspReportLoggerFactory : ICspReportLoggerFactory + { + private readonly ILogger _logger; + public CspReportLoggerFactory(ILogger logger) + { + _logger = logger; + } + + public CspReportLogger BuildLogger(LogLevel logLevel, string reportUri) + { + return new CspReportLogger + { + LogLevel = logLevel, + ReportUri = reportUri, + Logger = _logger + }; + } + } + + public class CspReportLogger + { + public virtual void Log(LogLevel logLevel, CspReport report) + { + Logger.Log(logLevel, TextualizeReport(report)); + } + + private string TextualizeReport(CspReport report) + { + return report.ToString(); + } + + public LogLevel LogLevel { get; internal set; } + public string ReportUri { get; internal set; } + public ILogger Logger { get; internal set; } + } +} diff --git a/src/Middleware/CSP/src/CspReportingMiddleware.cs b/src/Middleware/CSP/src/CspReportingMiddleware.cs index a5e0d94c97f3..f21f1a2c76c3 100644 --- a/src/Middleware/CSP/src/CspReportingMiddleware.cs +++ b/src/Middleware/CSP/src/CspReportingMiddleware.cs @@ -15,14 +15,12 @@ namespace Microsoft.AspNetCore.Csp { public class CspReportingMiddleware { - private readonly LoggingConfiguration _loggingConfig; - private readonly ILogger _logger; + private readonly CspReportLogger _loggingConfig; private readonly JsonSerializerOptions _serializerOptions; - public CspReportingMiddleware(RequestDelegate next, LoggingConfiguration loggingConfiguration, ILogger logger) + public CspReportingMiddleware(RequestDelegate next, CspReportLogger reportLogger) { - _loggingConfig = loggingConfiguration; - _logger = logger; + _loggingConfig = reportLogger; _serializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, @@ -33,7 +31,6 @@ public CspReportingMiddleware(RequestDelegate next, LoggingConfiguration logging private bool IsReportRequest(HttpRequest request) { - // TODO: Is this first condition guaranteed? return request.Path.StartsWithSegments(_loggingConfig.ReportUri) && request.ContentType?.StartsWith(CspConstants.CspReportContentType) == true && request.ContentLength != 0; @@ -46,7 +43,7 @@ private async void HandleIncomingReport(Stream body) CspReport cspReport = await JsonSerializer.DeserializeAsync(body, _serializerOptions); if (cspReport.ReportData != null) { - _logger.Log(_loggingConfig.LogLevel, TextualizeReport(cspReport, _loggingConfig.LogLevel)); + _loggingConfig.Log(_loggingConfig.LogLevel, cspReport); } } catch (JsonException) { @@ -54,12 +51,6 @@ private async void HandleIncomingReport(Stream body) } } - // TODO: Implement ToString on reportData - private string TextualizeReport(CspReport cspReport, LogLevel logLevel) - { - return cspReport.ReportData.ToString(); - } - public Task Invoke(HttpContext context) { if (IsReportRequest(context.Request)) @@ -68,8 +59,7 @@ public Task Invoke(HttpContext context) } context.Response.StatusCode = (int) HttpStatusCode.NoContent; - // TODO: Is there a better way to write an empty response? - return context.Response.WriteAsync(""); + return Task.FromResult(0); } } } diff --git a/src/Middleware/CSP/src/LoggingConfiguration.cs b/src/Middleware/CSP/src/LoggingConfiguration.cs deleted file mode 100644 index 12a5c83fc649..000000000000 --- a/src/Middleware/CSP/src/LoggingConfiguration.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Csp -{ - public class LoggingConfiguration - { - public LogLevel LogLevel { get; set; } - - public string ReportUri { get; set; } - } -} diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 54dca237caad..3c6bceddd6a7 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -157,7 +157,8 @@ public async void ProcessesMalformedReportRequestsCorrectly(string method, strin { await context.Response.WriteAsync("Test response"); }); - }); + }) + .ConfigureServices(services => services.AddCsp()); using (var server = new TestServer(hostBuilder)) { @@ -180,7 +181,8 @@ public async void ProcessesMalformedReportRequestsCorrectly(string method, strin public async void ProcessesCspReportRequestsCorrectly() { // Arrange - var logger = new Mock>(); + var mockLogger = new Mock(); + var loggerFactory = new FakeReportLoggerFactory(mockLogger.Object); var hostBuilder = new WebHostBuilder() .Configure(app => @@ -189,6 +191,7 @@ public async void ProcessesCspReportRequestsCorrectly() { policyBuilder .WithCspMode(CspMode.ENFORCING) + .WithLogLevel(LogLevel.Trace) .WithReportingUri("/cspreport"); }); app.Run(async context => @@ -196,9 +199,9 @@ public async void ProcessesCspReportRequestsCorrectly() await context.Response.WriteAsync("Test response"); }); }) - .ConfigureServices(services => - { - services.AddSingleton(typeof(ILogger), logger.Object); + .ConfigureServices(services => { + services.AddCsp(); + services.AddSingleton(loggerFactory); }); using (var server = new TestServer(hostBuilder)) @@ -226,8 +229,7 @@ public async void ProcessesCspReportRequestsCorrectly() // Assert Assert.Equal(204, context.Response.StatusCode); Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); - //TODO: ASSERT ON THE LOGGING STATEMENT! - //logger.Verify(m => m.Log(LogLevel.Information, "")); + mockLogger.Verify(m => m.Log(It.IsAny(), It.IsNotNull())); } } @@ -235,8 +237,6 @@ public async void ProcessesCspReportRequestsCorrectly() public async void DoesNotCollectReportsIfReportingUriIsNotRelative() { // Arrange - var logger = new Mock>(); - var hostBuilder = new WebHostBuilder() .ConfigureServices(services => services.AddCsp()) .Configure(app => @@ -252,10 +252,7 @@ public async void DoesNotCollectReportsIfReportingUriIsNotRelative() await context.Response.WriteAsync("Test response"); }); }) - .ConfigureServices(services => - { - services.AddSingleton(typeof(ILogger), logger.Object); - }); + .ConfigureServices(services => services.AddCsp()); using (var server = new TestServer(hostBuilder)) { @@ -285,4 +282,17 @@ public async void DoesNotCollectReportsIfReportingUriIsNotRelative() } } } + + public class FakeReportLoggerFactory : ICspReportLoggerFactory + { + readonly CspReportLogger logger; + public FakeReportLoggerFactory(CspReportLogger logger) + { + this.logger = logger; + } + public CspReportLogger BuildLogger(LogLevel logLevel, string reportUri) + { + return logger; + } + } } From 39fff82a968dae1161ddc5042e946e0f97f24bd5 Mon Sep 17 00:00:00 2001 From: Sal Date: Mon, 20 Jul 2020 16:21:08 +0000 Subject: [PATCH 21/26] Add CSP header on all requests --- src/Middleware/CSP/src/CspMiddleware.cs | 5 +-- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 43 ------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 447016aadf37..7918fcecebf6 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -17,10 +17,7 @@ public CspMiddleware(RequestDelegate next, ContentSecurityPolicy csp) public Task Invoke(HttpContext context, INonce nonce) { - if (context.Request.ContentType == null || context.Request.ContentType.Equals("text/html")) - { - context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(nonce); - } + context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(nonce); return _next(context); } } diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 3c6bceddd6a7..57b1f2fe124a 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -52,49 +52,6 @@ public async Task CspHeaderIsSetOnAllResponses(string requestPath) } } - [Theory] - [InlineData("text/html", true)] - [InlineData("application/json", false)] - [InlineData(null, true)] - public async Task CspHeaderIsSetOnlyOnValidResponses(string contentType, bool headerShouldExist) - { - // Arrange - var hostBuilder = new WebHostBuilder() - .ConfigureServices(services => services.AddCsp()) - .Configure(app => - { - app.UseCsp(policyBuilder => - { - policyBuilder - .WithCspMode(CspMode.ENFORCING); - }); - app.Run(async context => - { - await context.Response.WriteAsync("Test response"); - }); - }); - - using (var server = new TestServer(hostBuilder)) - { - // Act - var response = await server.CreateRequest("/").AddHeader("content-type", contentType) - .SendAsync("GET"); - - // Assert - response.EnsureSuccessStatusCode(); - if (headerShouldExist) - { - Assert.Single(response.Headers); - Assert.NotEmpty(response.Headers.GetValues(CspConstants.CspEnforcedHeaderName).FirstOrDefault()); - Assert.Equal("Test response", await response.Content.ReadAsStringAsync()); - } - else - { - Assert.Empty(response.Headers); - } - } - } - [Theory] [InlineData("/")] public async Task CspNonceExistsInHeader(string requestPath) From 38424fab3369b611fc8566fd69457b07563f5dc6 Mon Sep 17 00:00:00 2001 From: Sal Date: Tue, 21 Jul 2020 16:30:05 +0000 Subject: [PATCH 22/26] Code cleanup --- src/Middleware/CSP/src/ContentSecurityPolicy.cs | 3 --- src/Middleware/CSP/src/CspConstants.cs | 6 ------ src/Middleware/CSP/src/CspMiddleware.cs | 1 - .../CSP/src/CspMiddlewareExtensions.cs | 1 - src/Middleware/CSP/src/CspReportLogger.cs | 14 ++++++++++++-- src/Middleware/CSP/src/CspReportingMiddleware.cs | 16 +++++----------- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 2 +- .../Microsoft.AspNetCore.Csp.Test.csproj | 5 ----- 8 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index e38556c363e2..d75680802110 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace Microsoft.AspNetCore.Csp { diff --git a/src/Middleware/CSP/src/CspConstants.cs b/src/Middleware/CSP/src/CspConstants.cs index e738ea203b23..0abe8e80e21c 100644 --- a/src/Middleware/CSP/src/CspConstants.cs +++ b/src/Middleware/CSP/src/CspConstants.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.AspNetCore.Csp { public static class CspConstants diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 7918fcecebf6..35e58e809ada 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index 7d4431dc6991..bc8df1b64b76 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -1,7 +1,6 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp { diff --git a/src/Middleware/CSP/src/CspReportLogger.cs b/src/Middleware/CSP/src/CspReportLogger.cs index f52798e7b7f1..421b236aeb43 100644 --- a/src/Middleware/CSP/src/CspReportLogger.cs +++ b/src/Middleware/CSP/src/CspReportLogger.cs @@ -28,13 +28,23 @@ public CspReportLogger BuildLogger(LogLevel logLevel, string reportUri) public class CspReportLogger { - public virtual void Log(LogLevel logLevel, CspReport report) + public virtual void Log(CspReport report) { - Logger.Log(logLevel, TextualizeReport(report)); + Logger.Log(LogLevel, TextualizeReport(report)); } private string TextualizeReport(CspReport report) { + if (LogLevel.CompareTo(LogLevel.Information) > 0) + { + //switch (report.ReportData.ViolatedDirective) + //{ + + //} + } else + { + + } return report.ToString(); } diff --git a/src/Middleware/CSP/src/CspReportingMiddleware.cs b/src/Middleware/CSP/src/CspReportingMiddleware.cs index f21f1a2c76c3..cedf194d222e 100644 --- a/src/Middleware/CSP/src/CspReportingMiddleware.cs +++ b/src/Middleware/CSP/src/CspReportingMiddleware.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; -using System.Net.Mime; -using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp { @@ -43,9 +36,10 @@ private async void HandleIncomingReport(Stream body) CspReport cspReport = await JsonSerializer.DeserializeAsync(body, _serializerOptions); if (cspReport.ReportData != null) { - _loggingConfig.Log(_loggingConfig.LogLevel, cspReport); + _loggingConfig.Log(cspReport); } - } catch (JsonException) + } + catch (JsonException) { return; } @@ -58,7 +52,7 @@ public Task Invoke(HttpContext context) HandleIncomingReport(context.Request.Body); } - context.Response.StatusCode = (int) HttpStatusCode.NoContent; + context.Response.StatusCode = (int)HttpStatusCode.NoContent; return Task.FromResult(0); } } diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 57b1f2fe124a..07131c8766ac 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -186,7 +186,7 @@ public async void ProcessesCspReportRequestsCorrectly() // Assert Assert.Equal(204, context.Response.StatusCode); Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); - mockLogger.Verify(m => m.Log(It.IsAny(), It.IsNotNull())); + mockLogger.Verify(m => m.Log(It.IsNotNull())); } } diff --git a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj index d400d1ae1936..5355fd810a91 100644 --- a/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj +++ b/src/Middleware/CSP/test/UnitTests/Microsoft.AspNetCore.Csp.Test.csproj @@ -15,9 +15,4 @@ - - - - - \ No newline at end of file From 4e51fe7fe2d9f7550db4dd7f11662c245ae03304 Mon Sep 17 00:00:00 2001 From: Sal Date: Wed, 22 Jul 2020 14:12:02 +0000 Subject: [PATCH 23/26] Textualize CSP reports depending on the current log level. Add tests for this behaviour --- src/Middleware/CSP/src/CspConstants.cs | 3 + src/Middleware/CSP/src/CspReport.cs | 2 +- src/Middleware/CSP/src/CspReportLogger.cs | 63 ++++- .../CSP/src/CspReportingMiddleware.cs | 34 +-- .../CSP/test/UnitTests/CspIntegrationTests.cs | 38 --- .../CSP/test/UnitTests/CspMiddlewareTests.cs | 101 -------- .../CSP/test/UnitTests/CspReportLoggerTest.cs | 245 ++++++++++++++++++ .../CSP/test/UnitTests/TestUtils.cs | 46 ++++ 8 files changed, 353 insertions(+), 179 deletions(-) delete mode 100644 src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs create mode 100644 src/Middleware/CSP/test/UnitTests/CspReportLoggerTest.cs create mode 100644 src/Middleware/CSP/test/UnitTests/TestUtils.cs diff --git a/src/Middleware/CSP/src/CspConstants.cs b/src/Middleware/CSP/src/CspConstants.cs index 0abe8e80e21c..586a47fafcdc 100644 --- a/src/Middleware/CSP/src/CspConstants.cs +++ b/src/Middleware/CSP/src/CspConstants.cs @@ -5,5 +5,8 @@ public static class CspConstants public static readonly string CspEnforcedHeaderName = "Content-Security-Policy"; public static readonly string CspReportingHeaderName = "Content-Security-Policy-Report-Only"; public static readonly string CspReportContentType = "application/csp-report"; + public static readonly string ScriptSrcElem = "script-src-elem"; + public static readonly string BlockedUriInline = "inline"; + public static readonly string ScriptSrcAttr = "script-src-attr"; } } diff --git a/src/Middleware/CSP/src/CspReport.cs b/src/Middleware/CSP/src/CspReport.cs index 8ee8ef2107f5..57e2097940ac 100644 --- a/src/Middleware/CSP/src/CspReport.cs +++ b/src/Middleware/CSP/src/CspReport.cs @@ -21,7 +21,7 @@ public class Report [JsonPropertyName("source-file")] public string SourceFile { get; set; } [JsonPropertyName("line-number")] - public int LineNumber { get; set; } + public string LineNumber { get; set; } // Old browsers don't set the next two fields (e.g. Firefox v25/v26) [JsonPropertyName("original-policy")] diff --git a/src/Middleware/CSP/src/CspReportLogger.cs b/src/Middleware/CSP/src/CspReportLogger.cs index 421b236aeb43..6327e11c8672 100644 --- a/src/Middleware/CSP/src/CspReportLogger.cs +++ b/src/Middleware/CSP/src/CspReportLogger.cs @@ -1,3 +1,5 @@ +using System.IO; +using System.Text.Json; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp @@ -28,23 +30,64 @@ public CspReportLogger BuildLogger(LogLevel logLevel, string reportUri) public class CspReportLogger { - public virtual void Log(CspReport report) + private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions { - Logger.Log(LogLevel, TextualizeReport(report)); - } + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; - private string TextualizeReport(CspReport report) + public virtual async void Log(Stream jsonReport) { - if (LogLevel.CompareTo(LogLevel.Information) > 0) + if (LogLevel.CompareTo(LogLevel.Information) >= 0) { - //switch (report.ReportData.ViolatedDirective) - //{ + try + { + CspReport cspReport = await JsonSerializer.DeserializeAsync(jsonReport, _serializerOptions); + if (cspReport.ReportData != null) + { + Logger.Log(LogLevel, TextualizeReport(cspReport)); + } + } + catch (JsonException) + { + } - //} - } else - { + return; + } + + Logger.Log(LogLevel, new StreamReader(jsonReport).ReadToEnd()); + } + private string TextualizeReport(CspReport report) + { + if (LogLevel.CompareTo(LogLevel.Information) >= 0) + { + if (CspConstants.BlockedUriInline.Equals(report.ReportData.BlockedUri)) + { + // javascript: URI not allowed + if (CspConstants.ScriptSrcElem.Equals(report.ReportData.ViolatedDirective)) + { + return string.Format("Attempt to navigate to javascript URI from {0} was refused by policy", report.ReportData.DocumentUri); + } + // inline event handler + else if (CspConstants.ScriptSrcAttr.Equals(report.ReportData.ViolatedDirective)) + { + return string.Format("Inline event handler at {0} (line number {1}) was refused by policy", report.ReportData.DocumentUri, report.ReportData.LineNumber); + } + } + // script is missing or doesn't match nonce in the policy + else if (CspConstants.ScriptSrcElem.Equals(report.ReportData.ViolatedDirective)) + { + return string.Format( + "Script at {0} (line {1}) trying to load {2} refused to run due to missing or mismatching nonce value", + report.ReportData.DocumentUri, + report.ReportData.LineNumber, + report.ReportData.BlockedUri + ); + } } + return report.ToString(); } diff --git a/src/Middleware/CSP/src/CspReportingMiddleware.cs b/src/Middleware/CSP/src/CspReportingMiddleware.cs index cedf194d222e..2098887f8069 100644 --- a/src/Middleware/CSP/src/CspReportingMiddleware.cs +++ b/src/Middleware/CSP/src/CspReportingMiddleware.cs @@ -1,6 +1,4 @@ -using System.IO; using System.Net; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -8,48 +6,26 @@ namespace Microsoft.AspNetCore.Csp { public class CspReportingMiddleware { - private readonly CspReportLogger _loggingConfig; - private readonly JsonSerializerOptions _serializerOptions; + private readonly CspReportLogger _cspReportLogger; public CspReportingMiddleware(RequestDelegate next, CspReportLogger reportLogger) { - _loggingConfig = reportLogger; - _serializerOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true - }; + _cspReportLogger = reportLogger; } private bool IsReportRequest(HttpRequest request) { - return request.Path.StartsWithSegments(_loggingConfig.ReportUri) + return request.Path.StartsWithSegments(_cspReportLogger.ReportUri) + && request.Method == HttpMethods.Post && request.ContentType?.StartsWith(CspConstants.CspReportContentType) == true && request.ContentLength != 0; } - private async void HandleIncomingReport(Stream body) - { - try - { - CspReport cspReport = await JsonSerializer.DeserializeAsync(body, _serializerOptions); - if (cspReport.ReportData != null) - { - _loggingConfig.Log(cspReport); - } - } - catch (JsonException) - { - return; - } - } - public Task Invoke(HttpContext context) { if (IsReportRequest(context.Request)) { - HandleIncomingReport(context.Request.Body); + _cspReportLogger.Log(context.Request.Body); } context.Response.StatusCode = (int)HttpStatusCode.NoContent; diff --git a/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs b/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs deleted file mode 100644 index 40aecdfe947e..000000000000 --- a/src/Middleware/CSP/test/UnitTests/CspIntegrationTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Xunit; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Testing; -using CspApplication; -using Microsoft.AspNetCore.TestHost; -using System.IO; -using Microsoft.AspNetCore.Hosting; - -namespace Microsoft.AspNetCore.Csp.Test -{ - public class CspIntegrationTests : IClassFixture> - { - - private readonly WebApplicationFactory _factory; - - public CspIntegrationTests(WebApplicationFactory factory) - { - _factory = factory; - } - - [Theory] - [InlineData("/")] - public async Task CspNonceAddedToScriptTags(string requestPath) - { - // Arrange - var client = _factory.CreateClient(); - - // Act - var response = await client.GetAsync(requestPath); - - // Assert - response.EnsureSuccessStatusCode(); - //var header = response.Headers.GetValues(CspConstants.CspEnforcedHeaderName).FirstOrDefault(); - //Assert.NotEmpty(header); - //Assert.Matches("'nonce-", header); - } - } -} diff --git a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs index 07131c8766ac..d225d5cee00b 100644 --- a/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs +++ b/src/Middleware/CSP/test/UnitTests/CspMiddlewareTests.cs @@ -89,107 +89,6 @@ public async Task CspNonceExistsInHeader(string requestPath) } } - [Theory] - [InlineData("GET", "foo")] - [InlineData("GET", "application/csp-report")] - [InlineData("POST", "foo")] - [InlineData("POST", "application/csp-report")] - [InlineData("PUT", "foo")] - [InlineData("PUT", "application/csp-report")] - [InlineData("HEAD", "foo")] - [InlineData("HEAD", "application/csp-report")] - public async void ProcessesMalformedReportRequestsCorrectly(string method, string contentType) - { - // Arrange - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCsp(policyBuilder => - { - policyBuilder - .WithCspMode(CspMode.ENFORCING) - .WithReportingUri("/cspreport"); - }); - app.Run(async context => - { - await context.Response.WriteAsync("Test response"); - }); - }) - .ConfigureServices(services => services.AddCsp()); - - using (var server = new TestServer(hostBuilder)) - { - // Act - var context = await server.SendAsync(c => - { - c.Request.Method = method; - c.Request.Path = "/cspreport"; - c.Request.Headers[HeaderNames.ContentType] = contentType; - c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("malformed")); - }); - - // Assert - Assert.Equal(204, context.Response.StatusCode); - Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); - } - } - - [Fact] - public async void ProcessesCspReportRequestsCorrectly() - { - // Arrange - var mockLogger = new Mock(); - var loggerFactory = new FakeReportLoggerFactory(mockLogger.Object); - - var hostBuilder = new WebHostBuilder() - .Configure(app => - { - app.UseCsp(policyBuilder => - { - policyBuilder - .WithCspMode(CspMode.ENFORCING) - .WithLogLevel(LogLevel.Trace) - .WithReportingUri("/cspreport"); - }); - app.Run(async context => - { - await context.Response.WriteAsync("Test response"); - }); - }) - .ConfigureServices(services => { - services.AddCsp(); - services.AddSingleton(loggerFactory); - }); - - using (var server = new TestServer(hostBuilder)) - { - // Act - var context = await server.SendAsync(c => - { - c.Request.Method = "POST"; - c.Request.Path = "/cspreport"; - c.Request.Headers[HeaderNames.ContentType] = CspConstants.CspReportContentType; - c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes( - @"{ - ""csp-report"": { - ""document-uri"": ""http://example.com/signup.html"", - ""referrer"": ""http://evil.com"", - ""blocked-uri"": ""http://example.com/css/style.css"", - ""violated-directive"": ""style-src cdn.example.com"", - ""original-policy"": ""default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports"", - ""disposition"": ""report"" - } - }" - )); - }); - - // Assert - Assert.Equal(204, context.Response.StatusCode); - Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); - mockLogger.Verify(m => m.Log(It.IsNotNull())); - } - } - [Fact] public async void DoesNotCollectReportsIfReportingUriIsNotRelative() { diff --git a/src/Middleware/CSP/test/UnitTests/CspReportLoggerTest.cs b/src/Middleware/CSP/test/UnitTests/CspReportLoggerTest.cs new file mode 100644 index 000000000000..75b36fab8206 --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/CspReportLoggerTest.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; +using static Microsoft.AspNetCore.Csp.Test.TestUtils; + +namespace Microsoft.AspNetCore.Csp.Test +{ + public class CspReportLoggerTest + { + [Theory] + [InlineData("GET", "foo")] + [InlineData("GET", "application/csp-report")] + [InlineData("POST", "foo")] + [InlineData("POST", "application/csp-report")] + [InlineData("PUT", "foo")] + [InlineData("PUT", "application/csp-report")] + [InlineData("HEAD", "foo")] + [InlineData("HEAD", "application/csp-report")] + public async void ProcessesMalformedReportRequestsCorrectly(string method, string contentType) + { + // Arrange + var testLogger = new CspTestLogger(); + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING) + .WithReportingUri("/cspreport"); + }); + app.Run(async context => + { + await context.Response.WriteAsync("Test response"); + }); + }) + .ConfigureServices(services => { + services.AddCsp(); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + var context = await server.SendAsync(c => + { + c.Request.Method = method; + c.Request.Path = "/cspreport"; + c.Request.Headers[HeaderNames.ContentType] = contentType; + c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("malformed")); + }); + + // Assert + Assert.Equal(204, context.Response.StatusCode); + Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); + testLogger.NoLogStatementsMade(); + } + } + + [Fact] + public async void ProcessesCspReportRequestsCorrectly() + { + // Arrange + var mockLogger = new Mock(); + var loggerFactory = new FakeReportLoggerFactory(mockLogger.Object); + + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCsp(policyBuilder => + { + policyBuilder + .WithCspMode(CspMode.ENFORCING) + .WithLogLevel(LogLevel.Trace) + .WithReportingUri("/cspreport"); + }); + app.Run(async context => + { + await context.Response.WriteAsync("Test response"); + }); + }) + .ConfigureServices(services => { + services.AddCsp(); + services.AddSingleton(loggerFactory); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + var context = await server.SendAsync(c => + { + c.Request.Method = "POST"; + c.Request.Path = "/cspreport"; + c.Request.Headers[HeaderNames.ContentType] = CspConstants.CspReportContentType; + c.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes( + @"{ + ""csp-report"": { + ""document-uri"": ""http://example.com/signup.html"", + ""referrer"": ""http://evil.com"", + ""blocked-uri"": ""http://example.com/css/style.css"", + ""violated-directive"": ""style-src cdn.example.com"", + ""original-policy"": ""default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports"", + ""disposition"": ""report"" + } + }" + )); + }); + + // Assert + Assert.Equal(204, context.Response.StatusCode); + Assert.Empty(new StreamReader(context.Response.Body).ReadToEnd()); + mockLogger.Verify(m => m.Log(It.IsNotNull())); + } + } + + [Theory] + [ClassData(typeof(ReportLoggerTestData))] + public void LogsTextualRepresentationOfReportWhenLoggingLevelAboveOrEqualToInformation(string expectedDescription, string jsonReport, LogLevel logLevel) + { + // Arrange + var testLogger = new CspTestLogger(); + var factory = new CspReportLoggerFactory(testLogger); + var logger = factory.BuildLogger(logLevel, "reportUri"); + + // Act + logger.Log(new MemoryStream(Encoding.ASCII.GetBytes(jsonReport))); + + // Assert + testLogger.SingleLogStatementMatching(logLevel, expectedDescription); + } + + + [Theory] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Trace)] + public void LogsFullRepresentationOfReportWhenLoggingLevelBelowOrEqualToDebug(LogLevel logLevel) + { + // Arrange + var testLogger = new CspTestLogger(); + var factory = new CspReportLoggerFactory(testLogger); + var logger = factory.BuildLogger(logLevel, "reportUri"); + var jsonReport = @"{ + ""csp-report"": { + ""document-uri"": ""http://documenturi"", + ""referrer"": ""http://referrer"", + ""blocked-uri"": ""http://cdn/script.js"", + ""source-file"": ""source.js"", + ""line-number"": ""123"", + ""violated-directive"": ""script-src-elem"", + ""original-policy"": ""script-src 'nonce-abc' http: https:; report-uri /_/csp-reports"", + ""disposition"": ""report"" + } + }"; + + // Act + logger.Log(new MemoryStream(Encoding.ASCII.GetBytes(jsonReport))); + + // Assert + testLogger.SingleLogStatementMatching(logLevel, jsonReport); + } + } + + class ReportLoggerTestData : IEnumerable + { + List logLevels = new List + { + LogLevel.Information, + LogLevel.Warning, + LogLevel.Error, + LogLevel.Critical + }; + + List reports = new List + { + // script block missing nonce + @"{ + ""csp-report"": { + ""document-uri"": ""http://documenturi"", + ""referrer"": ""http://referrer"", + ""blocked-uri"": ""http://cdn/script.js"", + ""source-file"": ""source.js"", + ""line-number"": ""123"", + ""violated-directive"": ""script-src-elem"", + ""original-policy"": ""script-src 'nonce-abc' http: https:; report-uri /_/csp-reports"", + ""disposition"": ""report"" + } + }", + // javascript: URI sample + @"{ + ""csp-report"": { + ""document-uri"": ""http://documenturi"", + ""referrer"": ""http://referrer"", + ""blocked-uri"": ""inline"", + ""violated-directive"": ""script-src-elem"", + ""original-policy"": ""script-src 'nonce-abc' http: https:; report-uri /_/csp-reports"", + ""disposition"": ""report"" + } + }", + // inline javascript (event handler) + @"{ + ""csp-report"": { + ""document-uri"": ""http://documenturi"", + ""referrer"": ""http://referrer"", + ""blocked-uri"": ""inline"", + ""line-number"": ""123"", + ""violated-directive"": ""script-src-attr"", + ""script-sample"": ""const a = 1"", + ""original-policy"": ""script-src 'nonce-abc' http: https:; report-uri /_/csp-reports"", + ""disposition"": ""report"" + } + }", + }; + + List expectedDescriptions = new List + { + "Script at http://documenturi (line 123) trying to load http://cdn/script.js refused to run due to missing or mismatching nonce value", + "Attempt to navigate to javascript URI from http://documenturi was refused by policy", + "Inline event handler at http://documenturi (line number 123) was refused by policy" + }; + public IEnumerator GetEnumerator() + { + return expectedDescriptions + // match reports and expected descriptions + .Zip(reports, (d, r) => new { d, r }) + // find all combinations of the previous with each log level + .SelectMany(reportAndDesc => logLevels.Select(l => new object[] { reportAndDesc.d, reportAndDesc.r, l })) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Middleware/CSP/test/UnitTests/TestUtils.cs b/src/Middleware/CSP/test/UnitTests/TestUtils.cs new file mode 100644 index 000000000000..570faa4db616 --- /dev/null +++ b/src/Middleware/CSP/test/UnitTests/TestUtils.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Csp.Test +{ + public class TestUtils + { + public class CspTestLogger : ILogger + { + public Dictionary actualLogCalls = new Dictionary(); + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + throw new NotImplementedException(); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + actualLogCalls.Add(logLevel, formatter.Invoke(state, exception)); + } + + public void SingleLogStatementMatching(LogLevel logLevel, string expected) + { + Assert.Single(actualLogCalls); + + string actualMessage; + actualLogCalls.TryGetValue(logLevel, out actualMessage); + Assert.Equal(expected, actualMessage); + } + + public void NoLogStatementsMade() + { + Assert.Empty(actualLogCalls); + } + } + } +} From 22feb96c98ef0699f0b2af36543770c80bb90688 Mon Sep 17 00:00:00 2001 From: Sal Date: Thu, 23 Jul 2020 08:32:15 +0000 Subject: [PATCH 24/26] Remove debug nonce --- src/Middleware/CSP/src/NoncedScriptTagHelper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Middleware/CSP/src/NoncedScriptTagHelper.cs b/src/Middleware/CSP/src/NoncedScriptTagHelper.cs index 4611cd8e0739..06ca7ef203c7 100644 --- a/src/Middleware/CSP/src/NoncedScriptTagHelper.cs +++ b/src/Middleware/CSP/src/NoncedScriptTagHelper.cs @@ -14,9 +14,7 @@ public NoncedScriptTagHelper(INonce nonce) public override void Process(TagHelperContext context, TagHelperOutput output) { - //base.Process(context, output); output.TagName = "script"; - output.Attributes.SetAttribute("debug-nonce", _nonce.GetValue()); output.Attributes.SetAttribute("nonce", _nonce.GetValue()); } } From fa5dbfa7f2845f6b535bffd3039eb068f9f7d19a Mon Sep 17 00:00:00 2001 From: Sal Date: Thu, 23 Jul 2020 11:39:57 +0000 Subject: [PATCH 25/26] Add documentation --- .../CSP/src/ContentSecurityPolicy.cs | 27 +++++++++++++----- .../CSP/src/ContentSecurityPolicyBuilder.cs | 12 ++++++++ src/Middleware/CSP/src/CspConstants.cs | 24 ++++++++++++++++ src/Middleware/CSP/src/CspMiddleware.cs | 11 ++++++++ .../CSP/src/CspMiddlewareExtensions.cs | 17 +++++++++++ src/Middleware/CSP/src/CspReport.cs | 28 ++++++++++++++++++- src/Middleware/CSP/src/CspReportLogger.cs | 13 +++++++++ .../CSP/src/CspReportingMiddleware.cs | 15 ++++++++++ src/Middleware/CSP/src/INonce.cs | 14 ++++++++-- .../CSP/src/NoncedScriptTagHelper.cs | 5 ++++ .../test/testassets/CspApplication/Startup.cs | 2 +- 11 files changed, 156 insertions(+), 12 deletions(-) diff --git a/src/Middleware/CSP/src/ContentSecurityPolicy.cs b/src/Middleware/CSP/src/ContentSecurityPolicy.cs index d75680802110..3534b57a9804 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicy.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicy.cs @@ -1,15 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Text; namespace Microsoft.AspNetCore.Csp { - public enum CspMode - { - NONE, - REPORTING, - ENFORCING - } - + /// + /// A greedy Content Security Policy generator + /// public class ContentSecurityPolicy { private readonly string _baseAndObject = "base-uri 'none'; object-src 'none'"; @@ -20,6 +19,13 @@ public class ContentSecurityPolicy private readonly bool _unsafeEval; private readonly string _reportingUri; + /// + /// Instantiates a new . + /// + /// Represents whether the current policy is in enforcing or reporting mode. + /// Whether the policy should enable nonce propagation. + /// Whether JavaScript's eval should be allowed to run. + /// An absolute or relative URI representing the reporting endpoint public ContentSecurityPolicy( CspMode cspMode, bool strictDynamic, @@ -56,4 +62,11 @@ public string GetPolicy(INonce nonce) return policyBuilder.Invoke(nonce); } } + + public enum CspMode + { + NONE, + REPORTING, + ENFORCING + } } diff --git a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs index cb9bcfc29bad..1c574c394e1f 100644 --- a/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs +++ b/src/Middleware/CSP/src/ContentSecurityPolicyBuilder.cs @@ -1,8 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp { + /// + /// Allows customizing content security policies + /// public class ContentSecurityPolicyBuilder { private CspMode _cspMode; @@ -41,6 +47,12 @@ public ContentSecurityPolicyBuilder WithLogLevel(LogLevel logLevel) return this; } + /// + /// Whether the policy specifies a relative reporting URI. + /// + /// + /// If this method returns true, a handler for the reporting endpoint will be automatically added to this application. + /// public bool HasLocalReporting() { return _reportingUri != null && _reportingUri.StartsWith("/"); diff --git a/src/Middleware/CSP/src/CspConstants.cs b/src/Middleware/CSP/src/CspConstants.cs index 586a47fafcdc..4ec6fbc2aecf 100644 --- a/src/Middleware/CSP/src/CspConstants.cs +++ b/src/Middleware/CSP/src/CspConstants.cs @@ -1,12 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + namespace Microsoft.AspNetCore.Csp { + /// + /// CSP-related constants. + /// public static class CspConstants { + /// + /// CSP header name in enforcement mode. + /// public static readonly string CspEnforcedHeaderName = "Content-Security-Policy"; + /// + /// CSP header name in reporting mode. + /// public static readonly string CspReportingHeaderName = "Content-Security-Policy-Report-Only"; + /// + /// Expected content type for requests containing CSP violation reports. + /// public static readonly string CspReportContentType = "application/csp-report"; + /// + /// Possible violated directive value used to create textual representations of violation reports. + /// public static readonly string ScriptSrcElem = "script-src-elem"; + /// + /// Possible blocked URI value used to create textual representations of violation reports. + /// public static readonly string BlockedUriInline = "inline"; + /// + /// Possible violated directive value used to create textual representations of violation reports. + /// public static readonly string ScriptSrcAttr = "script-src-attr"; } } diff --git a/src/Middleware/CSP/src/CspMiddleware.cs b/src/Middleware/CSP/src/CspMiddleware.cs index 35e58e809ada..1305a034faa1 100644 --- a/src/Middleware/CSP/src/CspMiddleware.cs +++ b/src/Middleware/CSP/src/CspMiddleware.cs @@ -1,13 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Csp { + /// + /// Middleware for supporting CSP. + /// public class CspMiddleware { private readonly RequestDelegate _next; private readonly ContentSecurityPolicy _csp; + /// + /// Instantiates a new . + /// + /// The next middleware in the pipeline. + /// A content security policy generator. public CspMiddleware(RequestDelegate next, ContentSecurityPolicy csp) { _next = next; diff --git a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs index bc8df1b64b76..eceb1c4bc31d 100644 --- a/src/Middleware/CSP/src/CspMiddlewareExtensions.cs +++ b/src/Middleware/CSP/src/CspMiddlewareExtensions.cs @@ -1,11 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Csp { + /// + /// Extends to add CSP middleware support. + /// public static class CspMiddlewareExtensions { + /// + /// Adds a CSP middleware to this web application pipeline that will add a custom policy to responses and collect CSP violation reports sent by user agents. + /// + /// The IApplicationBuilder passed to the Configure method + /// A delegate to build a custom content security policy + /// The original app parameter public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action configurePolicy) { if (app == null) @@ -28,6 +40,11 @@ public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action(policyBuilder.Build()); } + /// + /// Adds the necessary bindings for CSP. Namely, allows adding nonces to script tags automatically and provides a custom logging factory. + /// + /// The IApplicationBuilder passed to the Configure method + /// The original services parameter public static IServiceCollection AddCsp(this IServiceCollection services) { if (services == null) diff --git a/src/Middleware/CSP/src/CspReport.cs b/src/Middleware/CSP/src/CspReport.cs index 57e2097940ac..8cf93b7c993a 100644 --- a/src/Middleware/CSP/src/CspReport.cs +++ b/src/Middleware/CSP/src/CspReport.cs @@ -1,13 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.Csp { + /// + /// An object representing a CSP violation report. + /// public class CspReport { [JsonPropertyName("csp-report")] public Report ReportData { get; set; } - // TODO: if we find a way to get the csp-report field from the JSON we'll be able to remove one level of nestedness public class Report { [JsonPropertyName("blocked-uri")] @@ -21,6 +28,7 @@ public class Report [JsonPropertyName("source-file")] public string SourceFile { get; set; } [JsonPropertyName("line-number")] + [JsonConverter(typeof(NumberToStringConverter))] public string LineNumber { get; set; } // Old browsers don't set the next two fields (e.g. Firefox v25/v26) @@ -36,4 +44,22 @@ public class Report public string Disposition { get; set; } } } + + class NumberToStringConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + return reader.GetInt32().ToString(); + } + + return reader.GetString(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } } diff --git a/src/Middleware/CSP/src/CspReportLogger.cs b/src/Middleware/CSP/src/CspReportLogger.cs index 6327e11c8672..5c6605b39a55 100644 --- a/src/Middleware/CSP/src/CspReportLogger.cs +++ b/src/Middleware/CSP/src/CspReportLogger.cs @@ -1,14 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.IO; using System.Text.Json; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Csp { + /// + /// CSP report logger factory used to provide custom report loggers that handle violation reports. + /// public interface ICspReportLoggerFactory { CspReportLogger BuildLogger(LogLevel logLevel, string reportUri); } + /// + /// Default CSP report logger factory implementation. Registers a CspReportLogger as the default handler. + /// public class CspReportLoggerFactory : ICspReportLoggerFactory { private readonly ILogger _logger; @@ -28,6 +37,10 @@ public CspReportLogger BuildLogger(LogLevel logLevel, string reportUri) } } + /// + /// This CSP report logger marshals JSON violation reports asynchronously into objects that can be turned into textual descriptions of these reports, explaining their root cause. + /// This behavior is controlled by the log level set on the content security policy. + /// public class CspReportLogger { private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions diff --git a/src/Middleware/CSP/src/CspReportingMiddleware.cs b/src/Middleware/CSP/src/CspReportingMiddleware.cs index 2098887f8069..f2434204284d 100644 --- a/src/Middleware/CSP/src/CspReportingMiddleware.cs +++ b/src/Middleware/CSP/src/CspReportingMiddleware.cs @@ -1,13 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Csp { + /// + /// CSP middleware used to collect violation reports sent by user agents. Enabled automatically when a relative reporting URI is set on the CSP policy. + /// public class CspReportingMiddleware { private readonly CspReportLogger _cspReportLogger; + /// + /// Instantiates a new . + /// + /// The next middleware in the pipeline. + /// A custom logger that allows extending this middleware's logging capabilities. public CspReportingMiddleware(RequestDelegate next, CspReportLogger reportLogger) { _cspReportLogger = reportLogger; @@ -21,6 +32,10 @@ private bool IsReportRequest(HttpRequest request) && request.ContentLength != 0; } + + /// + /// Handle incoming violation reports. Returns a 204 response regardless of whether the report is valid. + /// public Task Invoke(HttpContext context) { if (IsReportRequest(context.Request)) diff --git a/src/Middleware/CSP/src/INonce.cs b/src/Middleware/CSP/src/INonce.cs index 86a1076de2fc..ca0c640d3203 100644 --- a/src/Middleware/CSP/src/INonce.cs +++ b/src/Middleware/CSP/src/INonce.cs @@ -1,22 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; namespace Microsoft.AspNetCore.Csp { + /// + /// Simple interface representing a CSP nonce value. + /// public interface INonce { string GetValue(); } + /// + /// Default nonce implementation. Computes a random nonce and returns its base64 value on instantiation. + /// public class Nonce : INonce { private readonly string _value; - // TODO: Make sure we use an actually crypto-safe random generator. private static readonly Lazy _gen = new Lazy(() => new Random()); public Nonce() { - // TODO: Actually come up with an alphanumeric string of enough entropy rather than casting a random int to string. - _value = _gen.Value.Next().ToString(); + byte[] bytes = new byte[7]; + _gen.Value.NextBytes(bytes); + _value = Convert.ToBase64String(bytes); } public string GetValue() diff --git a/src/Middleware/CSP/src/NoncedScriptTagHelper.cs b/src/Middleware/CSP/src/NoncedScriptTagHelper.cs index 06ca7ef203c7..bb2c5d0643a5 100644 --- a/src/Middleware/CSP/src/NoncedScriptTagHelper.cs +++ b/src/Middleware/CSP/src/NoncedScriptTagHelper.cs @@ -1,7 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.TagHelpers; namespace Microsoft.AspNetCore.Csp { + /// + /// Tag helper used to automatically a nonce attribute to existing script tags. This is required to support nonce-based content security policies. + /// [HtmlTargetElement("script")] public class NoncedScriptTagHelper : TagHelper { diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs index 04afdc741dd9..ab292d46cf9a 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs +++ b/src/Middleware/CSP/test/testassets/CspApplication/Startup.cs @@ -32,7 +32,7 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // CSP configuration. Must come first because other middleware might skip any following middleware. - app.UseCsp(policyBuilder => policyBuilder.WithCspMode(CspMode.REPORTING) + app.UseCsp(policyBuilder => policyBuilder.WithCspMode(CspMode.ENFORCING) .WithReportingUri("/csp")); From d6eaa92fbcbf7a2e0f8c71ba12ecb502f712f011 Mon Sep 17 00:00:00 2001 From: aaron <5382864+aaronshim@users.noreply.github.com> Date: Fri, 24 Jul 2020 00:57:35 +0000 Subject: [PATCH 26/26] Simplify sample site. --- .../CspApplication/Pages/Index.cshtml | 12 +--- .../Pages/Shared/_Layout.cshtml | 24 ------- .../Shared/_ValidationScriptsPartial.cshtml | 2 - .../CspApplication/wwwroot/css/site.css | 71 ------------------- .../CspApplication/wwwroot/js/example.js | 4 +- .../CspApplication/wwwroot/js/site.js | 6 -- 6 files changed, 6 insertions(+), 113 deletions(-) delete mode 100644 src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml delete mode 100644 src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css delete mode 100644 src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml index 9f1024ee1a33..208646442324 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Index.cshtml @@ -8,19 +8,13 @@

    Welcome

    Learn about building Web apps with ASP.NET Core.

    - -
      + +

      Inline javascript has not run!

      diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml index 0f584c3ce124..52997f731758 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml +++ b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_Layout.cshtml @@ -4,29 +4,9 @@ @ViewData["Title"] - CspApplication -
      -
      @@ -39,9 +19,5 @@ © 2020 - CspApplication - Privacy
      - - - - @RenderSection("Scripts", required: false) diff --git a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml b/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml deleted file mode 100644 index 5a16d80a9aa7..000000000000 --- a/src/Middleware/CSP/test/testassets/CspApplication/Pages/Shared/_ValidationScriptsPartial.cshtml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css deleted file mode 100644 index e679a8ea7fb5..000000000000 --- a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/css/site.css +++ /dev/null @@ -1,71 +0,0 @@ -/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification -for details on configuring this project to bundle and minify static web assets. */ - -a.navbar-brand { - white-space: normal; - text-align: center; - word-break: break-all; -} - -/* Provide sufficient contrast against white background */ -a { - color: #0366d6; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.nav-pills .nav-link.active, .nav-pills .show > .nav-link { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -/* Sticky footer styles --------------------------------------------------- */ -html { - font-size: 14px; -} -@media (min-width: 768px) { - html { - font-size: 16px; - } -} - -.border-top { - border-top: 1px solid #e5e5e5; -} -.border-bottom { - border-bottom: 1px solid #e5e5e5; -} - -.box-shadow { - box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); -} - -button.accept-policy { - font-size: 1rem; - line-height: inherit; -} - -/* Sticky footer styles --------------------------------------------------- */ -html { - position: relative; - min-height: 100%; -} - -body { - /* Margin bottom by footer height */ - margin-bottom: 60px; -} -.footer { - position: absolute; - bottom: 0; - width: 100%; - white-space: nowrap; - line-height: 60px; /* Vertically center the text there */ -} diff --git a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/example.js b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/example.js index c0bc6fb320f5..f516978c79e2 100644 --- a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/example.js +++ b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/example.js @@ -1 +1,3 @@ -alert("I'm a link-loaded script!"); +document.addEventListener('DOMContentLoaded', e => { + document.getElementById('js-link-area').innerHTML = "Link-loaded javascript has run!"; +}); diff --git a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js b/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js deleted file mode 100644 index 580cf0f85fd6..000000000000 --- a/src/Middleware/CSP/test/testassets/CspApplication/wwwroot/js/site.js +++ /dev/null @@ -1,6 +0,0 @@ -// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification -// for details on configuring this project to bundle and minify static web assets. - -// Write your Javascript code. - -alert("I'm a link-loaded script!");