This article shows how to improve the security of an ASP.NET Core Blazor application by adding security headers to all HTTP Razor Page responses (Blazor WASM hosted in a ASP.NET Core hosted backend). The security headers are added using the NetEscapades.AspNetCore.SecurityHeaders Nuget package from Andrew Lock. The headers are used to protect the session, not for authentication. The application is authenticated using OpenID Connect, the security headers are used to protected the session. The authentication is implemented in the Blazor application using the BFF pattern. The WASM client part is just a view of the server rendered trusted backend and cookies are used in the browser. All API calls are same domain only and protected with a cookie and same site.

Code: https://github.com/damienbod/AspNetCore6Experiments

Blogs in this series

The NetEscapades.AspNetCore.SecurityHeaders and the NetEscapades.AspNetCore.SecurityHeaders.TagHelpers Nuget packages are added to the csproj file of the web application. The tag helpers are added to use the nonce from the CSP in the Razor Pages.

 <ItemGroup> 	<PackageReference  		Include="NetEscapades.AspNetCore.SecurityHeaders"  		Version="0.16.0" /> 	<PackageReference  		Include="NetEscapades.AspNetCore.SecurityHeaders.TagHelpers"  		Version="0.16.0" /> </ItemGroup> 

The Blazor definition is very similar to the ASP.NET Core Razor Page one. The main difference is that the CSP script policy is almost disabled due to the Blazor script requirements. We can at least force self on the content security policy header script-src definition.

The Blazor WASM logout link sends a HTTP Form POST request which is redirected to the OpenID Connect identity provider. The CSP needs to allow this redirect and the content secure policy form definition allows this.

 public static HeaderPolicyCollection GetHeaderPolicyCollection(      bool isDev, string identityProviderHost) { 	var policy = new HeaderPolicyCollection() 		.AddFrameOptionsDeny() 		.AddXssProtectionBlock() 		.AddContentTypeOptionsNoSniff() 		.AddReferrerPolicyStrictOriginWhenCrossOrigin() 		.RemoveServerHeader() 		.AddCrossOriginOpenerPolicy(builder => 		{ 			builder.SameOrigin(); 		}) 		.AddCrossOriginEmbedderPolicy(builder => 		{ 			builder.RequireCorp(); 		}) 		.AddCrossOriginResourcePolicy(builder => 		{ 			builder.SameOrigin(); 		}) 		.AddContentSecurityPolicy(builder => 		{ 			builder.AddObjectSrc().None(); 			builder.AddBlockAllMixedContent(); 			builder.AddImgSrc().Self().From("data:"); 			builder.AddFormAction().Self().From(identityProviderHost); 			builder.AddFontSrc().Self(); 			builder.AddStyleSrc().Self().UnsafeInline(); 			builder.AddBaseUri().Self(); 			builder.AddFrameAncestors().None();  			// due to Blazor 			builder.AddScriptSrc().Self().UnsafeInline().UnsafeEval(); 		}) 		.RemoveServerHeader() 		.AddPermissionsPolicy(builder => 		{ 			builder.AddAccelerometer().None(); 			builder.AddAutoplay().None();  			builder.AddCamera().None(); 			builder.AddEncryptedMedia().None(); 			builder.AddFullscreen().All(); 			builder.AddGeolocation().None(); 			builder.AddGyroscope().None(); 			builder.AddMagnetometer().None(); 			builder.AddMicrophone().None(); 			builder.AddMidi().None(); 			builder.AddPayment().None(); 			builder.AddPictureInPicture().None(); 			builder.AddSyncXHR().None(); 			builder.AddUsb().None(); 		});  	if (!isDev) 	{ 		// maxage = one year in seconds 		policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 60 * 60 * 24 * 365); 	}  	return policy; } 

Blazor adds the following script to the WASM host file. This means the CSP for scripts cannot be implemented in a good way.

 <script> var Module; window.__wasmmodulecallback__(); delete window.__wasmmodulecallback__; </script>   

script-src (from CSP evaluator)

'self' can be problematic if you host JSONP, Angular or user uploaded files.
'unsafe-inline' allows the execution of unsafe in-page scripts and event handlers.
'unsafe-eval' allows the execution of code injected into DOM APIs such as eval().

The aspnetcore-browser-refresh.js is also added for hot reload. This also prevents a strong CSP script definition in development. This could be fixed with a dev check in the policy definition. There is no point fixing this, until the wasmmodulecallback script bug is fixed.

I am following the ASP.NET Core issue and hope this can be improved for Blazor.

In the Startup class, the UseSecurityHeaders method is used to apply the HTTP headers policy and add the middleware to the application. The env.IsDevelopment() is used to add or not to add the HSTS header. The default HSTS middleware from the ASP.NET Core templates was removed from the Configure method as this is not required.

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { 	if (env.IsDevelopment()) 	{ 		app.UseDeveloperExceptionPage(); 	} 	else 	{ 		app.UseExceptionHandler("/Error"); 	}  	app.UseSecurityHeaders( 		SecurityHeadersDefinitions 			.GetHeaderPolicyCollection(env.IsDevelopment(),                     Configuration["AzureAd:Instance"])); 

The server header can be removed in the program class file of the Blazor server project if using Kestrel. If using IIS, you probably need to use the web.config to remove this.

 public static IHostBuilder CreateHostBuilder(string[] args) => 	Host.CreateDefaultBuilder(args) 		.ConfigureWebHostDefaults(webBuilder => 		{ 			webBuilder 				.ConfigureKestrel(options =>  					options.AddServerHeader = false) 				.UseStartup<Startup>(); 		}); 

When we scan the https://securityheaders.com/ you can view the results. You might need to disable the authentication to check this, or provide a public view.

The content security policy has a warning due to the script definition which is required for Blazor.

The https://csp-evaluator.withgoogle.com/ also displays a high severity finding due the the CSP script definition.

Notes:

If the application is fully protected without any public views, the follow redirects checkbox on the security headers needs to be disabled as then you only get the results of the identity provider used to authenticate.

I block all traffic, if possible, which is not from my domain including sub domains. If implementing enterprise applications, I would always do this. If implementing public facing applications with high traffic volumes or need extra fast response times, or need to reduce the costs of hosting, then CDNs would need to be used, allowed and so on. Try to block all first and open up as required and maybe you can avoid some nasty surprises from all the Javascript, CSS frameworks used.

Maybe until the CSP script is fixed for Blazor, you probably should avoid using Blazor for high security applications and use ASP.NET Core Razor Page applications instead.

If you use Blazor together with tokens in Azure AD or Azure B2C and this CSP script bug, you leave yourself open to having your tokens stolen. I would recommend using server authentication with Azure which removes the tokens from the browser and also solves the Azure SPA logout problem. Azure AD, Azure B2C do not support the revocation endpoint or introspection, so it is impossible to invalidate your tokens on a logout. It does not help if the IT admin, Azure monitoring can invalidate tokens using CAE.

Links

https://securityheaders.com/

https://csp-evaluator.withgoogle.com/

Security by Default Chrome developers

A Simple Guide to COOP, COEP, CORP, and CORS

https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders

https://github.com/dotnet/aspnetcore/issues/34428

https://w3c.github.io/webappsec-trusted-types/dist/spec/

https://web.dev/trusted-types/

https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies

https://docs.google.com/document/d/1zDlfvfTJ_9e8Jdc8ehuV4zMEu9ySMCiTGMS9y0GU92k/edit

https://scotthelme.co.uk/coop-and-coep/

https://github.com/OWASP/ASVS