blog post image
Andrew Lock avatar

Andrew Lock

~8 min read

How to add default security headers in ASP.NET Core using custom middleware

One of the easiest ways to harden and improve the security of a web application is through the setting of certain HTTP header values. As these headers are often added by the server hosting the application (e.g. IIS, Apache, NginX), they are normally configured at this level rather than directly in your code.

In ASP.NET 4, there was also the possibility of adding to the <system.webServer> element in web.config:

<system.webServer>
  <httpProtocol>
      <customHeaders>
        <add name="X-Frame-Options" value="SAMEORIGIN" />
        <add name="X-XSS-Protection" value="1; mode=block" />
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>
</system.webServer>

This allows you to set the X-Frame-Options, X-XSS-Protection, X-Content-Type-Options and Strict-Transport-Security headers and remove the X-Powered-By header at the application level, without having to modify your IIS server configuration directly. While X-Powered-By isn't always included in lists of headers to remove, for example on https://securityheaders.io, it is not required by browsers and anything that makes it harder to fingerprint your server is probably a good idea.

In ASP.NET Core, web.config has gone so this approach will no longer work (though you can still set the headers at the server level). However the configuration of your app in Startup.cs is far more explicit than before, which opens up another avenue whereby to modify the headers - the middleware. In this post I'm going to show how you can easily extend the existing middleware to add your own security headers to requests.

Update - as of RC2, the <web.config> file is back for IIS! That means that middleware is no longer necessarily the easiest way to customise headers per application in this case. However the middleware approach described here is far more extensible than using <customHeaders> so may well still find use cases. Also, using middleware ensures the headers are still added when you are self hosting Kestrel rather than hooking in to IIs.

You can find all the code for this post on GitHub, or as a NuGet package at NetEscapades.AspNetCode.SecurityHeaders. All feedback, comments or suggestions gratefully received!

Update - the NuGet package and project have been updated to the RTM version, and has undergone a bit of refactoring compared to this post, however the core principles behind it remain unchanged. You can check it out on GitHub.

What is Middleware?

The concept of middleware is probably pretty well understood in the world of node and express.js where it is a fundamental concept for building a web server, but for .NET developers it may be slightly less familiar. Middleware is essentially everything that sits in between the server HTTP pipe and your application proper, executing actions in your controllers in the case of MVC.

Middleware composition in ASP.NET core

Middleware is composed of simple modular blocks that are each handed the HTTP request in turn, process it in some way, and then either return a response directly or hands off to the next block. In this way you can build easily composable blocks that provide a different part of the process. For example, one block could check for authentication credentials, another could simple log the request somewhere etc

In ASP.NET Core, the middleware is defined in Startup.cs in the Configure method, in which each of the extension methods called on the IApplicationBuilder adds another module to the pipeline. The default pipeline as of RC2 is:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Creating a middleware component

In order to create a new middleware component, you must do two things:

  1. Create a class with the appropriate signature
  2. Register the class on the IApplicationBuilder

Creating the middleware class

A middleware class is just a standard class, it does not implement an interface as such, but it must conform to a certain shape in order to be successfully called at runtime:

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

public class MyMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        await _next(context);
    }
}

So this class will be passed a RequestDelegate in the constructor, which is a pointer to the next piece of middleware in the pipeline. When a request is made to the server, the first piece of middleware registered in Startup.cs is called using Invoke and passed the context. When a piece of middleware finishes processing, it then directly invokes the next piece of middleware in the chain until a response is returned.

It is worth noting that you are free to pass additional services and objects required to process the request in to the constructor of your middleware class - these will be automatically found and instantiated using dependency injection.

Registering the middleware in Startup

The current standard approach to registering middleware in Startup is to create an extension method on IApplicationBuilder that registers the correct class. If you have additional configuration required to create the middleware, it is suggested to create an overload which takes a Builder object or configuration class. Note that it is no longer considered best practice to create an extension method which takes a delegate.

using System;
using Microsoft.AspNetCore.Builder;

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<MyMiddleware>();
    }
}

Now all that is required is to register your middleware in the Configure method using:

  app.UseMyMiddleware();

Building the SecurityHeaderMiddleware

So now we know how to create and edit a piece of middleware, lets create one for ourselves! You can find a sample project containing the middleware on GitHub but the key classes are discussed below, elided for brevity.

First we have a SecurityHeadersPolicy object, which is a simple class containing a list of the headers to add and remove:

public class SecurityHeadersPolicy
{
    public IDictionary<string, string> SetHeaders { get; } 
         = new Dictionary<string, string>();

    public ISet<string> RemoveHeaders { get; } 
        = new HashSet<string>();
}

Next we have the middleware itself, SecurityHeadersMiddleware. An instance of SecurityHeadersPolicy is passed to the constructor of our middleare and used to modify the response in the pipeline:

public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;
    private readonly SecurityHeadersPolicy _policy;

    public SecurityHeadersMiddleware(RequestDelegate next, SecurityHeadersPolicy policy)
    {
        _next = next;
        _policy = policy;
    }

    public async Task Invoke(HttpContext context)
    {        
        IHeaderDictionary headers = context.Response.Headers;

        foreach (var headerValuePair in _policy.SetHeaders)
        {
            headers[headerValuePair.Key] = headerValuePair.Value;
        }

        foreach (var header in _policy.RemoveHeaders)
        {
            headers.Remove(header);
        }

       await _next(context);
    }
}

In the Invoke method, the headers we have registered are set on the output, and the headers we wish to remove are removed.

It's important to note here that we are overwriting the header values in the response with the values we provide, so if a header has already been set in a previous piece of middleware, it will have our new value. It is also worth noting that we cannot remove headers which have not yet been added to the response. This means that the IIS header modification discussed at the beginning of the post has the last word!

So next we have the extension method to register our middleware:

public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseSecurityHeadersMiddleware(this IApplicationBuilder app, SecurityHeadersBuilder builder)
    {
        SecurityHeaderPolicy policy = builder.Build();
        return app.UseMiddleware<SecurityHeadersMiddleware>(policy);
    }
}

In this extension method we provide an instance of a SecurityHeadersBuilder which exposes a Build() method to return the SecurityHeaderPolicy object. This is later injected in to the constructor of the SecurityHeaderMiddleware and allows you to customise which headers are added and removed.

The builder class exposes a number of methods to allow you to fluently construct it, building up a SecurityHeaderPolicy slowly. The general methods are shown below along with the methods for configuring X-Frame-Options and removing the server tag. The full class contains a number of additional methods for configuring other headers and can be found here.


public class SecurityHeadersBuilder
{
    private readonly SecurityHeadersPolicy _policy = new SecurityHeadersPolicy();

    public SecurityHeadersBuilder AddDefaultSecurePolicy()
    {
        AddFrameOptionsDeny();
        AddXssProtectionBlock();
        AddContentTypeOptionsNoSniff();
        AddStrictTransportSecurityMaxAge();
        RemoveServerHeader();

        return this;
    }

    public SecurityHeadersBuilder AddFrameOptionsDeny()
    {
        _policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.Deny;
        return this;
    }

    public SecurityHeadersBuilder AddFrameOptionsSameOrigin()
    {
        _policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
        return this;
    }

    public SecurityHeadersBuilder AddFrameOptionsSameOrigin(string uri)
    {
        _policy.SetHeaders[FrameOptionsConstants.Header] = string.Format(FrameOptionsConstants.AllowFromUri, uri);
        return this;
    }

    public SecurityHeadersBuilder RemoveServerHeader()
    {
        _policy.RemoveHeaders.Add(ServerConstants.Header);
        return this;
    }

    public SecurityHeadersBuilder AddCustomHeader(string header, string value)
    {
        _policy.SetHeaders[header] = value;
        return this;
    }

    public SecurityHeadersBuilder RemoveHeader(string header)
    {
        _policy.RemoveHeaders.Add(header);
        return this;
    }

    public SecurityHeadersPolicy Build()
    {
        return _policy;
    }
}

With all these pieces in place, it is a simple case of configuring the middleware in Configure() in Startup.cs:

app.UseSecurityHeadersMiddleware(new SecurityHeadersBuilder()
  .AddDefaultSecurePolicy()
  .AddCustomHeader("X-My-Custom-Header", "So cool")
);

And we're all done!

Comparing the (abbreviated) output from a call to the homepage before and after adding the middleware gives:

Before:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET

After:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
X-My-Custom-Header:So cool
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000
X-Powered-By: ASP.NET

We can see that we've successfully added X-Frame-Options, X-XSS-Protection, X-Content-Type-Options and Strict-Transport-Security headers, and removed the Server header, not too shabby!

It's important to remember these headers are returned for every request, which is not necessarily appropriate (e.g. the Strict-Transport-Security) should only be returned over HTTPS, whereas with the above implementation they will be returned with every request.

Another thing to remember is that the order of the middleware being registered is important. To add to all requests, UseSecurityHeadersMiddleware should be called very early in the configuration pipeline, before other middleware has a chance to return a response. For example, if you add the UseSecurityHeadersMiddleware call after the call to UseStaticFiles(), then any requests for static files will not have the headers added, as the static file middleware will return a response before the security header middleware has been invoked. In that case calls which exercise the full MVC pipeline would have different security headers to static file requests.

One slight annoyance of the setup provided when running under IIS/IIS Express is the X-Powered-By, header which is added outside of the Startup.cs pipeline (it is added in applicationhost.config). This header is not available in context.Response.Headers even if our middleware is the last in the pipe, so we can't remove it using this method!

Summary

In this post I discussed how to create custom middleware in general. I then demonstrated sample classes that allow you to automatically add and remove headers to and from HTTP requests. This allows you to add headers such as X-Frame-Options and X-XSS-Protection to all your responses, while removing unnecessary headers like Server.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?