diff --git a/src/Aspire.Dashboard/Model/BrowserSecurityHeadersMiddleware.cs b/src/Aspire.Dashboard/Model/BrowserSecurityHeadersMiddleware.cs index fe7c865f4d..be64506435 100644 --- a/src/Aspire.Dashboard/Model/BrowserSecurityHeadersMiddleware.cs +++ b/src/Aspire.Dashboard/Model/BrowserSecurityHeadersMiddleware.cs @@ -14,24 +14,40 @@ namespace Aspire.Dashboard.Model; internal sealed class BrowserSecurityHeadersMiddleware { private readonly RequestDelegate _next; - private readonly string _cspContent; + private readonly string _cspContentHttp; + private readonly string _cspContentHttps; public BrowserSecurityHeadersMiddleware(RequestDelegate next, IHostEnvironment environment) { _next = next; + _cspContentHttp = GenerateCspContent(environment, isHttps: false); + _cspContentHttps = GenerateCspContent(environment, isHttps: true); + } + + private static string GenerateCspContent(IHostEnvironment environment, bool isHttps) + { // Based on Blazor documentation recommendations: // https://learn.microsoft.com/aspnet/core/blazor/security/content-security-policy#server-side-blazor-apps // Changes: // - style-src adds inline styles as they're used extensively by Blazor FluentUI. // - frame-src none added to prevent nesting in iframe. var content = "base-uri 'self'; " + - "img-src data: https:; " + "object-src 'none'; " + "script-src 'self'; " + "style-src 'self' 'unsafe-inline'; " + "frame-src 'none';"; + if (isHttps) + { + // Only allow https images when the site is served over https. + content += " img-src data: https:;"; + } + else + { + content += " img-src data: http: https:;"; + } + // default-src limits where content can fetched from. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src // This value stops BrowserLink and automatic hot reload from working in development. @@ -41,7 +57,7 @@ public BrowserSecurityHeadersMiddleware(RequestDelegate next, IHostEnvironment e content += " default-src 'self';"; } - _cspContent = content; + return content; } public Task InvokeAsync(HttpContext context) @@ -49,7 +65,9 @@ public Task InvokeAsync(HttpContext context) // Don't set browser security headers on OTLP requests. if (context.Features.Get() == null) { - context.Response.Headers.ContentSecurityPolicy = _cspContent; + context.Response.Headers.ContentSecurityPolicy = context.Request.IsHttps + ? _cspContentHttps + : _cspContentHttp; // Recommended best practice value: https://web.dev/articles/referrer-best-practices context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; diff --git a/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs b/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs index c36dee0226..9f3622cb5a 100644 --- a/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs +++ b/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs @@ -43,6 +43,24 @@ public async Task InvokeAsync_Production_DenyExternalFetch() Assert.Contains("default-src", httpContext.Response.Headers.ContentSecurityPolicy.ToString()); } + [Theory] + [InlineData("https", "img-src data: https:;")] + [InlineData("http", "img-src data: http: https:;")] + public async Task InvokeAsync_Scheme_ImageSourceChangesOnScheme(string scheme, string expectedContent) + { + // Arrange + var middleware = CreateMiddleware(environmentName: "Production"); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = scheme; + + // Act + await middleware.InvokeAsync(httpContext); + + // Assert + Assert.NotEqual(StringValues.Empty, httpContext.Response.Headers.ContentSecurityPolicy); + Assert.Contains(expectedContent, httpContext.Response.Headers.ContentSecurityPolicy.ToString()); + } + [Fact] public async Task InvokeAsync_Otlp_NotAdded() {