diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index 0d4a07fefef5..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; @@ -54,7 +55,8 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende var original = b.RequestDelegate; b.RequestDelegate = async context => { - context.Response.Headers.Add("Content-Security-Policy", $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"); + var headers = context.Response.Headers; + headers.ContentSecurityPolicy = StringValues.Concat(headers.ContentSecurityPolicy, $"frame-ancestors {options.ContentSecurityFrameAncestorsPolicy}"); await original(context); }; } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index a8d1b883c3eb..25812209add8 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -50,6 +50,43 @@ 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( + [ + "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( 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()