From 4c48eddfdb394a978973d2365b0a87237fd5fa69 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 18 Apr 2024 15:03:44 +0200 Subject: [PATCH 1/6] Fix multiple CSP policies --- ...mponentsEndpointConventionBuilderExtensions.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index 0d4a07fefef5..cdfa6f1e5ee9 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -54,7 +54,20 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende var original = b.RequestDelegate; b.RequestDelegate = async context => { - context.Response.Headers.Add("Content-Security-Policy", $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"); + if (context.Response.Headers.ContentSecurityPolicy.Count == 0) + { + context.Response.Headers.ContentSecurityPolicy = $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"; + } + else + { + var result = new string[context.Response.Headers.ContentSecurityPolicy.Count + 1]; + for (var i = 0; i < result.Length - 1; i++) + { + result[i] = context.Response.Headers.ContentSecurityPolicy[i]; + } + result[^1] = $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"; + context.Response.Headers.ContentSecurityPolicy = result; + } await original(context); }; } From 3e34de00421ca64e796b0cc1cdd0935b53461dbc Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 18 Apr 2024 15:22:08 +0200 Subject: [PATCH 2/6] Add an E2E test --- .../WebSocketCompressionTests.cs | 33 +++++++++++++++++++ .../RazorComponentEndpointsStartup.cs | 10 ++++++ 2 files changed, 43 insertions(+) diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index a8d1b883c3eb..ecd52187b386 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -50,6 +50,39 @@ public async Task EmbeddingServerAppInsideIframe_WorksAsync() Assert.DoesNotContain("Content-Security-Policy", response.Headers.Select(h => h.Key)); } } + + [Fact] + public async Task EmbeddingServerAppInsideIframe_WorksWithMultipleCspHeaders() + { + Navigate("/subdir/iframe?add-csp"); + + var logs = Browser.GetBrowserLogs(LogLevel.Severe); + + Assert.Empty(logs); + + // Get the iframe element from the page, and inspect its contents for a p element with id inside-iframe + var iframe = Browser.FindElement(By.TagName("iframe")); + Browser.SwitchTo().Frame(iframe); + Browser.Exists(By.Id("inside-iframe")); + + using var client = new HttpClient() { BaseAddress = _serverFixture.RootUri }; + var response = await client.GetAsync("/subdir/iframe?add-csp"); + response.EnsureSuccessStatusCode(); + + if (ExpectedPolicy != null) + { + Assert.Equal( + response.Headers.GetValues("Content-Security-Policy"), + [ + "script-src 'self' 'unsafe-inline'", + $"frame-ancestors {ExpectedPolicy}" + ]); + } + else + { + Assert.DoesNotContain("Content-Security-Policy", response.Headers.Select(h => h.Key)); + } + } } public abstract partial class BlockedWebSocketCompressionTests( diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 6dd3c4cb16b6..43c5947926af 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -68,6 +68,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); UseFakeAuthState(app); app.UseAntiforgery(); + + app.Use((ctx, nxt) => + { + if (ctx.Request.Query.ContainsKey("add-csp")) + { + ctx.Response.Headers.Add("Content-Security-Policy", "script-src 'self' 'unsafe-inline'"); + } + return nxt(); + }); + _ = app.UseEndpoints(endpoints => { _ = endpoints.MapRazorComponents() From bdc764024279d945833a0e335c36d9bbfe884ff5 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 18 Apr 2024 16:16:42 +0200 Subject: [PATCH 3/6] Update src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs Co-authored-by: Steve Sanderson --- ...ponentsEndpointConventionBuilderExtensions.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index cdfa6f1e5ee9..9363c25b1a33 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -54,20 +54,8 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende var original = b.RequestDelegate; b.RequestDelegate = async context => { - if (context.Response.Headers.ContentSecurityPolicy.Count == 0) - { - context.Response.Headers.ContentSecurityPolicy = $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"; - } - else - { - var result = new string[context.Response.Headers.ContentSecurityPolicy.Count + 1]; - for (var i = 0; i < result.Length - 1; i++) - { - result[i] = context.Response.Headers.ContentSecurityPolicy[i]; - } - result[^1] = $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"; - context.Response.Headers.ContentSecurityPolicy = result; - } + var headers = context.Response.Headers; + headers.ContentSecurityPolicy = StringValues.Concat(headers.ContentSecurityPolicy, $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"); await original(context); }; } From 9eb95db9222c9bf4c7e46eb233f688aee2097df1 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 18 Apr 2024 16:21:33 +0200 Subject: [PATCH 4/6] Cleanups --- .../WebSocketCompressionTests.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index ecd52187b386..6c555f6255b2 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -69,20 +69,13 @@ public async Task EmbeddingServerAppInsideIframe_WorksWithMultipleCspHeaders() var response = await client.GetAsync("/subdir/iframe?add-csp"); response.EnsureSuccessStatusCode(); - if (ExpectedPolicy != null) - { - Assert.Equal( - response.Headers.GetValues("Content-Security-Policy"), - [ - "script-src 'self' 'unsafe-inline'", - $"frame-ancestors {ExpectedPolicy}" - ]); - } - else { - Assert.DoesNotContain("Content-Security-Policy", response.Headers.Select(h => h.Key)); - } - } + Assert.Equal( + response.Headers.GetValues("Content-Security-Policy"), + [ + "script-src 'self' 'unsafe-inline'", + $"frame-ancestors {ExpectedPolicy}" + ]); } public abstract partial class BlockedWebSocketCompressionTests( From 9b2b5ad79e6070121f9ae0c8197377e599715e74 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 18 Apr 2024 16:26:53 +0200 Subject: [PATCH 5/6] Update src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs Co-authored-by: Steve Sanderson --- .../E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index 6c555f6255b2..3a6295cd14b9 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -69,7 +69,6 @@ public async Task EmbeddingServerAppInsideIframe_WorksWithMultipleCspHeaders() var response = await client.GetAsync("/subdir/iframe?add-csp"); response.EnsureSuccessStatusCode(); - { Assert.Equal( response.Headers.GetValues("Content-Security-Policy"), [ From 9bbfb2963c6a2fa0b44fb21c8292e5c6186199a1 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 18 Apr 2024 16:52:16 +0200 Subject: [PATCH 6/6] Fix build and test --- ...entsEndpointConventionBuilderExtensions.cs | 1 + .../WebSocketCompressionTests.cs | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index 9363c25b1a33..f317213cba0e 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Builder; diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index 3a6295cd14b9..25812209add8 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -69,12 +69,24 @@ public async Task EmbeddingServerAppInsideIframe_WorksWithMultipleCspHeaders() var response = await client.GetAsync("/subdir/iframe?add-csp"); response.EnsureSuccessStatusCode(); - Assert.Equal( - response.Headers.GetValues("Content-Security-Policy"), - [ - "script-src 'self' 'unsafe-inline'", - $"frame-ancestors {ExpectedPolicy}" - ]); + if (ExpectedPolicy != null) + { + Assert.Equal( + [ + "script-src 'self' 'unsafe-inline'", + $"frame-ancestors {ExpectedPolicy}" + ], + response.Headers.GetValues("Content-Security-Policy")); + } + else + { + Assert.Equal( + [ + "script-src 'self' 'unsafe-inline'" + ], + response.Headers.GetValues("Content-Security-Policy")); + } + } } public abstract partial class BlockedWebSocketCompressionTests(