The ASP.NET Core Security Headers Guide

Tue Mar 23, 2021

1 comment 3591 reads

ASP.NET Core / Tutorials

armin-zia

This post is a sum-up of how to make it more difficult for hackers to compromise your ASP.NET (MVC, Web API, Core) website in general. In the following paragraphs, I have listed a number of HTTP headers that are easily configured and that everyone should implement.

Security is not an afterthought and you should think about how to secure your applications from the start. There are many levels of security from authentication and authorization, to configuring web servers and firewalls, SSL certificates and encryption, etc. In this post, I want to show you how you can configure HTTP headers in your ASP.NET Core applications to improve security. These are standard headers that need to be implemented in all websites and web applications, regardless of the framework of use and how you've built your application.

If you're not already familiar with SecurityHeaders.com, check it out and add it to your toolbox. Created by Scott Helme, this handy tool scans your website and makes suggestions as to which HTTP response headers to add in order to improve security.

Let's start by discussing how to modify headers in ASP.NET Core. Like ASP.NET (MVC) there are multiple ways of modifying headers. This post introduces two different ways:

  1. Through middleware
  2. In web.config

Headers in middleware

This is my favorite approach, easy to use and elegant. Specifying headers in middleware can be done in C# code by creating one or more pieces of middleware. Most examples in this post will use this approach. In short, you either create a middleware class or call the Use method directly in the Configure method in Startup.cs:

app.Use(async (context, next) =>
{
    context.Response.Headers.Add("Header-Name", "Header-Value");
    await next();
};

The code above adds a header called Header-Name to all responses. It's important to call the Use method before calling UseEndpoints, UseMvc, and similar configurations.

A quick word about adding headers in middleware while also using the UseStatusCodePagesWithReExecute method. UseStatusCodePagesWithReExecute executes the configured error page within the same HTTP context as the original request. This means that your header adding middleware is executed twice. In this setup, make sure to check the Headers collection to avoid duplicates and exceptions:

if (!context.Response.Headers.ContainsKey("Header-Name"))
{
    context.Response.Headers.Add("Header-Name", "Header-Value");
}

Headers in web.config

ASP.NET Core no longer needs a web.config file, and application settings have moved to a JSON model. But since most people host their ASP.NET Core websites on IIS, a web.config file is still perfectly valid. Plus, if you're hosting on IIS, you'd probably need a web.config file anyway to configure IIS-related settings. While the system.web, appsettings, connectionStrings, and other root elements no longer apply, the system.webServer element is for the web servers' eyes only.

Custom headers in ASP.NET Core can be added by adding a web.config file to the root of the website directory and including the following code:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Header-Name" value="Header-Value" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

This configuration does exactly the same as we saw with the middleware code. This adds a header named Header-Name with the value of Header-Value. In some cases, you will need to use the web.config file to remove headers, I'll guide you through that in a moment.

Headers for improved security

I'll introduce you to a number of HTTP headers and how to configure them in ASP.NET Core, but make sure to read more about them to understand each header better.

X-Frame-Options

Hackers use iframes to trick your users into clicking unintended links. The X-Frame-Options header tells clients (web browsers) that framing isn't allowed. The header can be easily added using middleware:

context.Response.Headers.Add("X-Frame-Options", "DENY");

You can change the value to SAMEORIGIN to allow your website to iframe pages.

Blog posts throughout the web mention that the X-Frame-Options header is automatically added with the SAMEORIGIN value when enabling anti-forgery:

services.AddAntiforgery();

Either I'm doing something wrong or that feature was removed in recent ASP.NET Core releases. I don't see the header automatically being added. In any case, if you want full control of the header, make sure to disable the automatic feature:

services.AddAntiforgery(options =>
{
    options.SuppressXFrameOptionsHeader = true;
});

X-XSS-Protection

The X-XSS-Protection header will cause most modern browsers to stop loading the page when a cross-site scripting attack is identified. The header can be added through middleware:

context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");

The value 1 means enabled and the block mode will block the browser from rendering the page.

X-Content-Type-Options

MIME-type sniffing is an attack where a hacker tries to exploit missing metadata on served files. The header can be through middleware:

context.Response.Headers.Add("X-Content-Type-Options", "nosniff");

The value nosniff prevents primarily old browsers from MIME-sniffing.

If you want to cover static files as well, the header can be added to the web.config file instead:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="X-Content-Type-Options" value="nosniff" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Referrer-Policy

When you click a link on a website, the calling URL is automatically transferred to the linked site. Unless this is necessary, you should disable it using the Referrer-Policy header:

context.Response.Headers.Add("Referrer-Policy", "no-referrer");

There are a lot of possible values for this header, like same-origin that will set the referrer as long as the user stays on the same website.

X-Permitted-Cross-Domain-Policies

You're probably not using Flash, right? Right!? If not, you can disable the possibility of Flash making cross-site requests using the X-Permitted-Cross-Domain-Policies header:

context.Response.Headers.Add("X-Permitted-Cross-Domain-Policies", "none");

Strict-Transport-Security

All pages (and responses) should be served over HTTPS. To make sure that none of your content is served over HTTP, set the Strict-Transport-Security header. The header can be set using custom middleware like in the previous examples, but ASP.NET Core has a built-in middleware for this named HSTS (HTTP Strict Transport Security Protocol):

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // ...
    }
    else
    {
        app.UseHsts();
    }
}

As shown in the code above, it is recommended to use HSTS in production only, to avoid possible issues when developing locally. You can customize the Strict-Transport-Security header's values too:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    
    services.AddHsts(options =>
    {
        options.IncludeSubDomains = true;
        options.MaxAge = TimeSpan.FromDays(365);
    });
}

The cove above will produce a header with subdomains included and a max-age of 1 year:

Strict-Transport-Security: max-age=31536000; includeSubDomains

X-Powered-By

Like ASP.NET, ASP.NET Core returns the X-Powered-By header when you host your website on IIS. This means that you cannot remove the header in middleware, since this is managed outside of ASP.NET Core. web.config to the rescue:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Server

Like X-Powered-By, IIS kindly identifies itself in the Server header. While hackers could probably quickly find out anyway, you should still make it harder for them by removing this header. There's a dedicated security feature available in web.config for doing that:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <security>
      <requestFiltering removeServerHeader="true" />
    </security>
  </system.webServer>
</configuration>

Permissions-Policy

The Permissions-Policy header (formerly known as Feature-Policy) tells the browser which platform features your website needs. Most web applications won't need to access the microphone, or the vibrator functions available on mobile browsers. Why not be explicit about it to avoid imported scripts or framed pages, doing things you don't expect?

context.Response.Headers.Add("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");

There have been big changes since this was called Feature-Policy, make sure to have a read.

Content-Security-Policy

The Content-Security-Policy header (CSP) needs a long post for itself, but basically, this header helps to prevent code injection attacks like cross-site scripting and clickjacking, by telling the browser which dynamic resources are allowed to load. You can add this header easily via middleware:

context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'");

Apart from how you configure this header, you'd need to make sure you've added the domains that your application relies on too. For instance, if your website loads scripts and stylesheets from cdnjs, you need to correctly add the domain to your CSP, otherwise, the resources will be blocked by the browser.

ASP.NET Core Middleware Guide

Creating custom middleware couldn't be easier. Say you have an ASP.NET Core web application, create a folder named Middleware and create a class named PoweredByMiddleware.cs:

using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace Pegasus.WebUI.Middleware
{
    public class PoweredByMiddleware
    {
        private readonly RequestDelegate _next;

        public PoweredByMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext httpContext)
        {
            httpContext.Response.Headers["X-Powered-By"] = "Pegasus";
            return _next.Invoke(httpContext);
        }
    }
}

This piece of middleware adds the X-Powered-By header with the value of Pegasus. Now how do we use it? Open your Startup.cs file and add the following statement to the Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseMiddleware<PoweredByMiddleware>();
  // ...
}

Brilliant, that's all there is to it. Let's create a middleware that encapsulates the security headers we just discussed. Create a class named SecurityHeadersMiddleware.cs with the following code:

using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace Pegasus.WebUI.Middleware
{
    public class SecurityHeadersMiddleware
    {
        private readonly RequestDelegate _next;

        public SecurityHeadersMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext httpContext)
        {
            // X-Frame-Options
            if (!httpContext.Response.Headers.ContainsKey("X-Frame-Options"))
            {
                httpContext.Response.Headers.Add("X-Frame-Options", "DENY");
            }

            // X-Xss-Protection
            if (!httpContext.Response.Headers.ContainsKey("X-XSS-Protection"))
            {
                httpContext.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
            }

            // X-Content-Type-Options
            if (!httpContext.Response.Headers.ContainsKey("X-Content-Type-Options"))
            {
                httpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff");
            }

            // Referrer-Policy
            if (!httpContext.Response.Headers.ContainsKey("Referrer-Policy"))
            {
                httpContext.Response.Headers.Add("Referrer-Policy", "no-referrer");
            }

            // X-Permitted-Cross-Domain-Policies
            if (!httpContext.Response.Headers.ContainsKey("X-Permitted-Cross-Domain-Policies"))
            {
                httpContext.Response.Headers.Add("X-Permitted-Cross-Domain-Policies", "none");
            }

            // Permissions-Policy
            if (!httpContext.Response.Headers.ContainsKey("Permissions-Policy"))
            {
                httpContext.Response.Headers.Add("Permissions-Policy", "accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'");
            }

            // Content-Security-Policy
            if (!httpContext.Response.Headers.ContainsKey("Content-Security-Policy"))
            {
                httpContext.Response.Headers.Add("Content-Security-Policy", "form-action 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com https://code.jquery.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net");
            }

            return _next.Invoke(httpContext);
        }
    }
}

Before adding each header, the code checks whether the header exists in the Headers collection or not, this is to prevent duplicates and runtime exceptions. The Content-Security-Policy header shows a basic example, but you'd need to modify the domains that your application makes use of. You use the middleware just as you saw in the previous example:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseMiddleware<SecurityHeadersMiddleware>();
  // ...
}

Lastly, here's a web.config file showing how you can remove the X-Powered-By and Server headers:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<system.webServer>
		<security>
			<requestFiltering removeServerHeader="true" />
		</security>
		<httpProtocol>
			<customHeaders>
				<remove name="X-Powered-By" />
			</customHeaders>
		</httpProtocol>
	</system.webServer>
</configuration>

Further reading

Posted in ASP.NET Core

Tagged Tutorials