Adding host filtering to Kestrel in ASP.NET Core
Tags: aspnetcore, hosting, security
Published: 2020-07-05 18:34:55

---
title: Adding host filtering to Kestrel in ASP.NET Core
published: true
date: 2020-06-16 09:00:00 UTC
tags: ASPNETCore,Hosting,Security
canonical_url: https://andrewlock.net/adding-host-filtering-to-kestrel-in-aspnetcore/
---

 ![Adding host filtering to Kestrel in ASP.NET Core](https://andrewlock.net/content/images/2020/host_filtering_banner.png)

Host filtering, restricting the hostnames that your app responds to, is recommended whenever you're running in production for security reasons. In this post, I describe how to add host filtering to an ASP.NET Core application.

## What is host filtering?

When you run an ASP.NET Core application using Kestrel, you can choose the _ports_ it binds to, [as I described in a previous post](https://andrewlock.net/5-ways-to-set-the-urls-for-an-aspnetcore-app/). You have a few options:

- Bind to a port on the loopback (localhost) address
- Bind to a port on a specific IP address
- Bind to a port on any IP address on the machine.

Note that we didn't mention a hostname (e.g. example.org) at any point here - and that's because Kestrel _doesn't_ bind to a specific host name, it just listens on a given port.

> Note that [HTTP.sys _can_ be used to bind to a specific hostname](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/httpsys?view=aspnetcore-3.1).

DNS is used to convert the hostname you type in your address bar to an IP address, and typically port 80, or (443 for HTTPS). You can simulate configuring DNS with a provider locally be editing the _hosts_ file on your local machine, as I'll show in the remainder of this section

On Linux, you can run `sudo nano /etc/hosts` to edit the hosts file. On Windows, open an administrative command prompt, and run `notepad C:\Windows\System32\drivers\etc\hosts`. At the bottom of the file, add entries for `site-a.local` and `site-b.local` that point to your local machine:

```bash
# Other existing configuration
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host

127.0.0.1 site-a.local
127.0.0.1 site-b.local
```

Now create a simple ASP.NET Core app, for example using `dotnet new webapi`. By default, if you run `dotnet run`, your application will listen on ports 5000 (HTTP) and 5001 (HTTPS), on all IP addresses. If you navigate to [https://localhost:5001/weatherforecast](https://localhost:5001/weatherforecast), you'll see the results from the standard default `WeatherForecastController`.

Thanks to your additions to the _hosts_ file, you can now _also_ access the site at _site-a.local_ and _site-b.local_, for example, [https://site-a.local:5001/weatherforecast](https://site-a.local:5001/weatherforecast):

![Image of accessing the site using the site-a.local domain](https://andrewlock.net/content/images/2020/host_filtering_01.png)

> You'll need to click through some SSL warnings to view the example above, as the development SSL is only valid for localhost, not our custom domain.

By default, there's no host filtering, so you can access the ASP.NET Core app via localhost, or any other hostname that maps to your IP address, such as our custom `site-a.local` domain. But why does that matter?

## Why should you use host filtering?

The Microsoft documentation points out that you should use host filtering for a couple of reasons:

- [When doing URL generation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.1#url-generation-concepts).
- [When hosting behind a reverse proxy and the `Host` header is forwarded correctly](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-3.1#host-filtering)

There are several attacks which rely on an apps responding to requests, regardless of the host name:

- [A DNS rebinding attack](https://benmmurphy.github.io/blog/2016/07/11/rails-webconsole-dns-rebinding/), that allows attackers to execute code on your machine if you're running a server on localhost.
- [Cache poisoning attacks](http://carlos.bueno.org/2008/06/host-header-injection.html)
- [Password reset hijacking](https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html)

The latter two attacks essentially rely on an application "echoing" the hostname used to access the website when generating URLs.

You can easily see this vulnerability in an ASP.NET Core app if you generate an absolute URL, for use in a password reset email for example. As a simple example, consider the controller below: this generates an absolute link to the WeatherForecast action (shown in the previous image):

> Specifying the protocol means an absolute URL is generated instead of a relative link. There are various other methods that generate absolute links, as well [as others on the `LinkGenerator`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.1#url-generation-concepts).

```csharp
[ApiController]
[Route("[controller]")]
public class ValuesController : Controller
{
    [HttpGet]
    public string GetPasswordReset()
    {
        return Url.Action("Get", "WeatherForecast", values: null, protocol: "https");
    }
}
```

Depending on the hostname you access the site with, a different link is generated. By leveraging "forgot your password" functionality, an attacker could send an email from your system to any of your users with a link to a malicious domain under the attacker's control!

![Image of generating links based on the hostname](https://andrewlock.net/content/images/2020/host_filtering_02.png)

Hopefully we can all agree that's bad… luckily the fix isn't hard.

## Enabling host filtering for Kestrel

Host filtering is added by default in the `ConfigureWebHostDefaults()` method, but it's disabled by default. If you're using this method, you can enable the middleware by setting the `"AllowedHosts"` value in the app's `IConfiguration`.

In the default templates, this value is set to `*` in _appsettings.json_, which disables the middleware. To add host filtering, add a semicolon delimited list of hostnames:

```json
{
  "AllowedHosts": "site-a.local;localhost"
}

```

> You can set the configuration value using any enabled configuration provider, for example using an environment variable.

With this value set, you can still access the allowed host names, but all other requests to other hosts will return a 400 response, stating the hostname is invalid:

![Hostnames not included in the AllowedHosts setting will return a 400](https://andrewlock.net/content/images/2020/host_filtering_03.png)

If you're not using the `ConfigureWebHostDefaults()` method, you need to configure the `HostFilteringOptions` yourself, and add the `HostFilteringMiddleware` manually to your middleware pipeline. You can configure these in `Startup.ConfigureServices()`. For example, the following uses the `"AllowedHosts"` configuration setting, in a similar way to the defaults:

```csharp
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // ..other config

        // "AllowedHosts": "localhost;127.0.0.1;[::1]"
        var hosts = Configuration["AllowedHosts"]?
                        .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
        if(hosts?.Length > 0)
        {
            services.Configure<HostFilteringOptions>(
                options => options.AllowedHosts = hosts;
        }
    }

    public void Configure(IApplicationBuilder app)
    {
        // Should be first in the pipeline
        app.UseHostFiltering();

        // .. other config

    }
}
```

This is very similar to the default configuration used by `ConfigureWebHostDefaults()`, though it doesn't allow changing the configured hosts at runtime. [See the default implementation](https://github.com/dotnet/aspnetcore/blob/5f7e1a0004/src/DefaultBuilder/src/WebHost.cs) if that's something you need.

> The default configuration uses an `IStartupFilter`, `HostFilteringStartupFilter`, to add the hosting middleware, but it's `internal`, so you'll have to make do with the approach above.

Host filtering is especially important when you're running Kestrel on the edge, without a reverse proxy, as in most cases a reverse proxy will manage the host filtering for you. Depending on the reverse proxy you use, you may need to set the `ForwardedHeadersOptions.AllowedHosts` value, to restrict the allowed values of the `X-Forwarded-Host` header. You can read more about configuring the forwarded headers and a reverse proxy [in the documentation](https://docs.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer).

## Summary

In this post I described Kestrel's default behaviour, of binding to a _port_ not a domain. I then showed how this behaviour can be used as an attack vector by generating malicious links, if you don't filter requests to only a limited number of hosts. Finally, I showed how to enable host filtering by setting the `AllowedHosts` value in configuration, or by manually adding the `HostFilteringMiddleware`.