Improving application security in Blazor using HTTP headers – Part 2

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

History

  • 2023-03-11 fix: applied security headers to all request
  • 2021-11-26 Updated CSP for Blazor , fixes from .NET 6 release

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.18.0" />
	<PackageReference 
		Include="NetEscapades.AspNetCore.SecurityHeaders.TagHelpers" 
		Version="0.18.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 weaker due to the Blazor script requirements.

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();
		})
        // remove for dev if using hot reload
		.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();
			builder.AddBaseUri().Self();
			builder.AddFrameAncestors().None();

			// due to Blazor
            builder.AddScriptSrc()
                .Self()
                .WithHash256("v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=")
                .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);
	}

	policy.ApplyDocumentHeadersToAllResponses();

	return policy;
}

Blazor adds the following script to the WASM host file.

<script src="_framework/dotnet.6.0.0.i0pg1fb8jh.js" 
defer=""
integrity="sha256-LOq52qjRa3+zt3w4UL10rS3UdwHxVQxW0JAsMPO9QAM=" 
crossorigin="anonymous">

</script>

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. If you do want to use hot reload in local developement, you must disable the CSP style and script checks, You MUST not deploy the application with the CSP disabled. Also do not allow inline styles.

I am following the ASP.NET Core issue and hope this can be improved for Blazor which has fixed some issues in the .NET 6 release. Hopefully the UnsafeEval definition can be fixed in the .NET 7 release.

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 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.

If you use Blazor together with tokens in Azure AD or Azure B2C and a weak CSP definition, 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

3 comments

  1. […] Improving application security in Blazor using HTTP headers – Part 2 […]

  2. […] Improving application security in Blazor using HTTP headers – Part 2 (Damien Bowden) […]

  3. […] New Videos Covering Vue 3.2 (Shawn Wildermuth) 5 Ways Azure Cognitive Services Scale (Amy Boyd) Improving application security in Blazor using HTTP headers – Part 2 (Damien Bowden) Easy Steps to Synchronize JIRA Calendar Tasks With the Blazor Scheduler (Mahesh […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: