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

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:

# 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, 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:

Image of accessing the site using the site-a.local domain

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:

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

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.

[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

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:

{
  "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

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:

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

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.