Improving application security in ASP.NET Core Razor Pages using HTTP headers – Part 1

This article shows how to improve the security of an ASP.NET Core Razor Page application by adding security headers to all HTTP Razor Page responses. 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 Open ID Connect, the security headers are used to protected the session.

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

Blogs in this series

History

2023-11-03 fix: applied security headers removed XSS block

2023-03-11 fix: applied security headers to all requests

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.21.0" />
	<PackageReference 
		Include="NetEscapades.AspNetCore.SecurityHeaders.TagHelpers" 
		Version="0.21.0" />
</ItemGroup>

What each header protects against is explained really good on the securityheaders.com results view of your test. You can click different links of each validation which takes you to the documentation of each header. The links at the bottom of this post provide some excellent information about what and why of the headers.

The security header definitions are added using the HeaderPolicyCollection class. I added this to a separate class to keep the Startup class small, where the middleware is added. I passed a boolean parameter into the method which is used to add or remove the HSTS header. We might not want to add this to local development and block all non HTTPS requests to localhost.

The policy defined in this demo is for Razor Page applications with as much blocked as possible. You should be able to re-use this in your projects.

The COOP (Cross Origin Opener Policy), COEP (Cross Origin Embedder Policy), CORP (Cross Origin Resource Policy) headers are relatively new. You might need to update you existing application deployments with these. The links at the bottom of the post provide information about these headers in detail and I would recommend reading these.

public static HeaderPolicyCollection GetHeaderPolicyCollection(bool isDev)
{
	var policy = new HeaderPolicyCollection()
		.AddFrameOptionsDeny()
		.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();
			builder.AddFontSrc().Self();
			builder.AddStyleSrc().Self(); // .UnsafeInline();
			builder.AddBaseUri().Self();
			builder.AddScriptSrc().UnsafeInline().WithNonce();
			builder.AddFrameAncestors().None();
		})
		.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;
}

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()));

The server header can be removed in the program class 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>();
		});

We want to apply the CSP nonce to all our scripts in the Razor Pages. We can add the NetEscapades.AspNetCore.SecurityHeaders namespace to the _ViewImports.cshtml file.

@using AspNetCoreRazor
@using NetEscapades.AspNetCore.SecurityHeaders
@namespace AspNetCoreRazor.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

In the Razor Page _Layout, the CSP nonce can be used. I had to get the nonce from the HttpContext and add this to all scripts. This way, the scripts will be loaded. If the nonce does not match or is not applied to the script, it will not be loaded due to the CSP definition. I’ll ping Andrew Lock to see if the tag helper could be used directly.

    @{var nonce = Context.GetNonce();}
    <script src="~/lib/jquery/dist/jquery.min.js" nonce="@nonce"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js" nonce="@nonce"></script>
    <script src="~/js/site.js" asp-append-version="true" nonce="@nonce"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

The Http security headers can be tested using https://securityheaders.com

I used ngrok to test this before I deployed the application. The Security headers scans the web application and returns a really neat summary to what headers you have and how good it finds them. Each header has a link to a excellent documentation, blog on Scott Helme‘s website https://scotthelme.co.uk

The CSP can be tested using the https://csp-evaluator.withgoogle.com from google. The CSP evaluator gives an excellent summary and also suggestions how to improve the CSP.

The Razor Page application security can be much improved by added the headers to the application. The NetEscapades.AspNetCore.SecurityHeaders Nuget package makes it incredibly easy to apply this. I will create a follow up blog with a policy definition for a Blazor application and also a for an Web API application.

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.

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

9 comments

  1. […] Improving application security in ASP.NET Core Razor Pages using HTTP headers – Part 1 […]

  2. […] Improving application security in ASP.NET Core Razor Pages using HTTP headers – Part 1Improving application security in Blazor using HTTP headers – Part 2Improving application security in an ASP.NET Core API using HTTP headers – Part 3 […]

  3. […] Improving application security in ASP.NET Core Razor Pages using HTTP headers – Part 1Improving application security in Blazor using HTTP headers – Part 2Improving application security in an ASP.NET Core API using HTTP headers – Part 3 […]

  4. Tony Fearn · · Reply

    I’m new to this and trying to update our scripts and styles with a nonce. What if you have a custom header, which already defines the content policy! Do I need to remove the custom header or will the startup code interact with it and update it?

  5. […] Improving application security in ASP.NET Core Razor Pages using HTTP headers – Part 1 Improving application security in Blazor using HTTP headers – Part 2 Improving application security in an ASP.NET Core API using HTTP headers – Part 3 […]

  6. […] Improving application security in ASP.NET Core Razor Pages using HTTP headers – Part 1 Improving application security in Blazor using HTTP headers – Part 2 Improving application security in an ASP.NET Core API using HTTP headers – Part 3 […]

  7. […] Improving application security in ASP.NET Core Razor Pages using HTTP headers – Part 1 Improving application security in Blazor using HTTP headers – Part 2 Improving application security in an ASP.NET Core API using HTTP headers – Part 3 […]

  8. I want to adopt your code in my aspnet app. Currently I’m using app.UseCors and app.UseHsts. Is it correct that I can now drop these lines because these are now handled by your code?

    1. Hi Merjin

      You can drop UseHsts, not User CORS, this is required if you have cross domain access, which maybe you don’t. PS, I just updated the headers config, required a fix for XSS

      greetings Damien

Leave a comment

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