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:
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.
- When hosting behind a reverse proxy and the
Host
header is forwarded correctly
There are several attacks which rely on an apps responding to requests, regardless of the host name:
- A DNS rebinding attack, that allows attackers to execute code on your machine if you're running a server on localhost.
- Cache poisoning attacks
- Password reset hijacking
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!
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:
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'sinternal
, 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
.