Add host filtering middleware

This commit is contained in:
Chris Ross (ASP.NET) 2018-02-05 15:38:55 -08:00
parent 8b58a9a091
commit 2b80c90554
21 changed files with 845 additions and 38 deletions

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26817.0
VisualStudioVersion = 15.0.27130.2027
MinimumVisualStudioVersion = 15.0.26730.03
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpOverrides", "src\Microsoft.AspNetCore.HttpOverrides\Microsoft.AspNetCore.HttpOverrides.csproj", "{517308C3-B477-4B01-B461-CAB9C10B6928}"
EndProject
@ -63,6 +63,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpsPolicySample", "sample
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsPolicy.Tests", "test\Microsoft.AspNetCore.HttpsPolicy.Tests\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", "{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostFilteringSample", "samples\HostFilteringSample\HostFilteringSample.csproj", "{368B00A2-992A-4B0E-9085-A8136A22922D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{5CEA6F31-A829-4A02-8CD5-EC3DDD4CC1EA}"
ProjectSection(SolutionItems) = preProject
build\dependencies.props = build\dependencies.props
build\repo.props = build\repo.props
build\sources.props = build\sources.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HostFiltering.Tests", "test\Microsoft.AspNetCore.HostFiltering.Tests\Microsoft.AspNetCore.HostFiltering.Tests.csproj", "{4BC947ED-13B8-4BE6-82A4-96A48D86980B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HostFiltering", "src\Microsoft.AspNetCore.HostFiltering\Microsoft.AspNetCore.HostFiltering.csproj", "{762F7276-C916-4111-A6C0-41668ABB3823}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -129,6 +142,18 @@ Global
{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}.Release|Any CPU.Build.0 = Release|Any CPU
{368B00A2-992A-4B0E-9085-A8136A22922D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{368B00A2-992A-4B0E-9085-A8136A22922D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{368B00A2-992A-4B0E-9085-A8136A22922D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{368B00A2-992A-4B0E-9085-A8136A22922D}.Release|Any CPU.Build.0 = Release|Any CPU
{4BC947ED-13B8-4BE6-82A4-96A48D86980B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BC947ED-13B8-4BE6-82A4-96A48D86980B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4BC947ED-13B8-4BE6-82A4-96A48D86980B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BC947ED-13B8-4BE6-82A4-96A48D86980B}.Release|Any CPU.Build.0 = Release|Any CPU
{762F7276-C916-4111-A6C0-41668ABB3823}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{762F7276-C916-4111-A6C0-41668ABB3823}.Debug|Any CPU.Build.0 = Debug|Any CPU
{762F7276-C916-4111-A6C0-41668ABB3823}.Release|Any CPU.ActiveCfg = Release|Any CPU
{762F7276-C916-4111-A6C0-41668ABB3823}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -149,6 +174,10 @@ Global
{4D39C29B-4EC8-497C-B411-922DA494D71B} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
{AC424AEE-4883-49C6-945F-2FC916B8CA1C} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE} = {8437B0F3-3894-4828-A945-A9187F37631D}
{368B00A2-992A-4B0E-9085-A8136A22922D} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{5CEA6F31-A829-4A02-8CD5-EC3DDD4CC1EA} = {59A9B64C-E9BE-409E-89A2-58D72E2918F5}
{4BC947ED-13B8-4BE6-82A4-96A48D86980B} = {8437B0F3-3894-4828-A945-A9187F37631D}
{762F7276-C916-4111-A6C0-41668ABB3823} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4518E9CE-3680-4E05-9259-B64EA7807158}

View File

@ -14,6 +14,7 @@
<MicrosoftAspNetCoreTestHostPackageVersion>2.1.0-preview2-30281</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftExtensionsConfigurationAbstractionsPackageVersion>2.1.0-preview2-30281</MicrosoftExtensionsConfigurationAbstractionsPackageVersion>
<MicrosoftExtensionsConfigurationBinderPackageVersion>2.1.0-preview2-30281</MicrosoftExtensionsConfigurationBinderPackageVersion>
<MicrosoftExtensionsConfigurationJsonPackageVersion>2.1.0-preview2-30281</MicrosoftExtensionsConfigurationJsonPackageVersion>
<MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>2.1.0-preview2-30281</MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.0-preview2-30281</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>2.1.0-preview2-30281</MicrosoftExtensionsLoggingConsolePackageVersion>

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.HostFiltering\Microsoft.AspNetCore.HostFiltering.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.Production.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace HostFilteringSample
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
var hostBuilder = new WebHostBuilder()
.ConfigureLogging((_, factory) =>
{
factory.SetMinimumLevel(LogLevel.Debug);
factory.AddConsole();
})
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
})
.UseKestrel()
.UseStartup<Startup>();
return hostBuilder.Build();
}
}
}

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:14124/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"HostFilteringSample": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:14125/"
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HostFilteringSample
{
public class Startup
{
public IConfiguration Config { get; }
public Startup(IConfiguration config)
{
Config = config;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddHostFiltering(options =>
{
// If this is excluded then it will fall back to the server's addresses
options.AllowedHosts = Config.GetSection("AllowedHosts").Get<List<string>>();
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHostFiltering();
app.Run(context =>
{
return context.Response.WriteAsync("Hello World! " + context.Request.Host);
});
}
}
}

View File

@ -0,0 +1,3 @@
{
"AllowedHosts": [ "localhost", "127.0.0.1", "[::1]" ]
}

View File

@ -0,0 +1,3 @@
{
"AllowedHosts": [ "example.com", "localhost" ]
}

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.HostFiltering;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for the HostFiltering middleware.
/// </summary>
public static class HostFilteringBuilderExtensions
{
/// <summary>
/// Adds middleware for filtering requests by allowed host headers. Invalid requests will be rejected with a
/// 400 status code.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
/// <returns>The original <see cref="IApplicationBuilder"/>.</returns>
public static IApplicationBuilder UseHostFiltering(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
app.UseMiddleware<HostFilteringMiddleware>();
return app;
}
}
}

View File

@ -0,0 +1,166 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.HostFiltering
{
/// <summary>
/// A middleware used to filter requests by their Host header.
/// </summary>
public class HostFilteringMiddleware
{
// Matches Http.Sys.
private static readonly byte[] DefaultResponse = Encoding.ASCII.GetBytes(
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\">\r\n"
+ "<HTML><HEAD><TITLE>Bad Request</TITLE>\r\n"
+ "<META HTTP-EQUIV=\"Content-Type\" Content=\"text/html; charset=us-ascii\"></ HEAD >\r\n"
+ "<BODY><h2>Bad Request - Invalid Hostname</h2>\r\n"
+ "<hr><p>HTTP Error 400. The request hostname is invalid.</p>\r\n"
+ "</BODY></HTML>");
private readonly RequestDelegate _next;
private readonly ILogger<HostFilteringMiddleware> _logger;
private readonly HostFilteringOptions _options;
private IList<StringSegment> _allowedHosts;
private bool? _allowAnyNonEmptyHost;
/// <summary>
/// A middleware used to filter requests by their Host header.
/// </summary>
/// <param name="next"></param>
/// <param name="logger"></param>
/// <param name="options"></param>
public HostFilteringMiddleware(RequestDelegate next, ILogger<HostFilteringMiddleware> logger,
IOptions<HostFilteringOptions> options)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Processes requests
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public Task Invoke(HttpContext context)
{
EnsureConfigured();
if (!CheckHost(context))
{
context.Response.StatusCode = 400;
if (_options.IncludeFailureMessage)
{
context.Response.ContentLength = DefaultResponse.Length;
context.Response.ContentType = "text/html";
return context.Response.Body.WriteAsync(DefaultResponse, 0, DefaultResponse.Length);
}
return Task.CompletedTask;
}
return _next(context);
}
private void EnsureConfigured()
{
if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0)
{
return;
}
var allowedHosts = new List<StringSegment>();
if (_options.AllowedHosts?.Count > 0 && !TryProcessHosts(_options.AllowedHosts, allowedHosts))
{
_logger.LogDebug("Wildcard detected, all requests with hosts will be allowed.");
_allowAnyNonEmptyHost = true;
return;
}
if (allowedHosts.Count == 0)
{
throw new InvalidOperationException("No allowed hosts were configured.");
}
_logger.LogDebug("Allowed hosts: " + string.Join("; ", allowedHosts));
_allowedHosts = allowedHosts;
}
// returns false if any wildcards were found
private bool TryProcessHosts(IEnumerable<string> incoming, IList<StringSegment> results)
{
foreach (var entry in incoming)
{
// Punycode. Http.Sys requires you to register Unicode hosts, but the headers contain punycode.
var host = new HostString(entry).ToUriComponent();
if (IsTopLevelWildcard(host))
{
// Disable filtering
return false;
}
if (!results.Contains(host, StringSegmentComparer.OrdinalIgnoreCase))
{
results.Add(host);
}
}
return true;
}
private bool IsTopLevelWildcard(string host)
{
return (string.Equals("*", host, StringComparison.Ordinal) // HttpSys wildcard
|| string.Equals("[::]", host, StringComparison.Ordinal) // Kestrel wildcard, IPv6 Any
|| string.Equals("0.0.0.0", host, StringComparison.Ordinal)); // IPv4 Any
}
// This does not duplicate format validations that are expected to be performed by the host.
private bool CheckHost(HttpContext context)
{
var host = new StringSegment(context.Request.Headers[HeaderNames.Host].ToString()).Trim();
if (StringSegment.IsNullOrEmpty(host))
{
// Http/1.0 does not require the host header.
// Http/1.1 requires the header but the value may be empty.
if (!_options.AllowEmptyHosts)
{
_logger.LogInformation($"{context.Request.Protocol} request rejected due to missing or empty host header.");
return false;
}
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug($"{context.Request.Protocol} request allowed with missing or empty host header.");
}
return true;
}
if (_allowAnyNonEmptyHost == true)
{
_logger.LogTrace($"All hosts are allowed.");
return true;
}
if (HostString.MatchesAny(host, _allowedHosts))
{
_logger.LogTrace($"The host '{host}' matches an allowed host.");
return true;
}
_logger.LogInformation($"The host '{host}' does not match an allowed host.");
return false;
}
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.HostFiltering
{
/// <summary>
/// Options for the HostFiltering middleware
/// </summary>
public class HostFilteringOptions
{
/// <summary>
/// The hosts headers that are allowed to access this site. At least one value is required.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><description>Port numbers must be excluded.</description></item>
/// <item><description>A top level wildcard "*" allows all non-empty hosts.</description></item>
/// <item><description>Subdomain wildcards are permitted. E.g. "*.example.com" matches subdomains like foo.example.com,
/// but not the parent domain example.com.</description></item>
/// <item><description>Unicode host names are allowed but will be converted to punycode for matching.</description></item>
/// <item><description>IPv6 addresses must include their bounding brackets and be in their normalized form.</description></item>
/// </list>
/// </remarks>
public IList<string> AllowedHosts { get; set; } = new List<string>();
/// <summary>
/// Indicates if requests without hosts are allowed. The default is true.
/// </summary>
/// <remarks>
/// HTTP/1.0 does not require a host header.
/// Http/1.1 requires a host header, but says the value may be empty.
/// </remarks>
public bool AllowEmptyHosts { get; set; } = true;
// Note if this were disabled then things like the status code middleware may try to re-execute
// the request. This is a low level protocol violation, pretty error pages should not be required.
/// <summary>
/// Indicates if the 400 response should include a default message or be empty. This is enabled by default.
/// </summary>
public bool IncludeFailureMessage { get; set; } = true;
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.HostFiltering;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for the host filtering middleware.
/// </summary>
public static class HostFilteringServicesExtensions
{
/// <summary>
/// Adds services and options for the host filtering middleware.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <param name="configureOptions">A delegate to configure the <see cref="HostFilteringOptions"/>.</param>
/// <returns></returns>
public static IServiceCollection AddHostFiltering(this IServiceCollection services, Action<HostFilteringOptions> configureOptions)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.Configure(configureOptions);
return services;
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
ASP.NET Core middleware for filtering out requests with unknown HTTP host headers.
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(MicrosoftAspNetCoreHostingAbstractionsPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.HttpOverrides
{
@ -22,6 +24,8 @@ namespace Microsoft.AspNetCore.HttpOverrides
private readonly ForwardedHeadersOptions _options;
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private bool _allowAllHosts;
private IList<StringSegment> _allowedHosts;
static ForwardedHeadersMiddleware()
{
@ -85,6 +89,8 @@ namespace Microsoft.AspNetCore.HttpOverrides
_options = options.Value;
_logger = loggerFactory.CreateLogger<ForwardedHeadersMiddleware>();
_next = next;
PreProcessHosts();
}
private static void EnsureOptionNotNullorWhitespace(string value, string propertyName)
@ -95,6 +101,43 @@ namespace Microsoft.AspNetCore.HttpOverrides
}
}
private void PreProcessHosts()
{
if (_options.AllowedHosts == null || _options.AllowedHosts.Count == 0)
{
_allowAllHosts = true;
return;
}
var allowedHosts = new List<StringSegment>();
foreach (var entry in _options.AllowedHosts)
{
// Punycode. Http.Sys requires you to register Unicode hosts, but the headers contain punycode.
var host = new HostString(entry).ToUriComponent();
if (IsTopLevelWildcard(host))
{
// Disable filtering
_allowAllHosts = true;
return;
}
if (!allowedHosts.Contains(host, StringSegmentComparer.OrdinalIgnoreCase))
{
allowedHosts.Add(host);
}
}
_allowedHosts = allowedHosts;
}
private bool IsTopLevelWildcard(string host)
{
return (string.Equals("*", host, StringComparison.Ordinal) // HttpSys wildcard
|| string.Equals("[::]", host, StringComparison.Ordinal) // Kestrel wildcard, IPv6 Any
|| string.Equals("0.0.0.0", host, StringComparison.Ordinal)); // IPv4 Any
}
public Task Invoke(HttpContext context)
{
ApplyForwarders(context);
@ -231,7 +274,8 @@ namespace Microsoft.AspNetCore.HttpOverrides
if (checkHost)
{
if (!string.IsNullOrEmpty(set.Host) && TryValidateHost(set.Host))
if (!string.IsNullOrEmpty(set.Host) && TryValidateHost(set.Host)
&& (_allowAllHosts || HostString.MatchesAny(set.Host, _allowedHosts)))
{
applyChanges = true;
currentValues.Host = set.Host;

View File

@ -67,6 +67,22 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public IList<IPNetwork> KnownNetworks { get; } = new List<IPNetwork>() { new IPNetwork(IPAddress.Loopback, 8) };
/// <summary>
/// The allowed values from x-forwarded-host. If the list is empty then all hosts are allowed.
/// Failing to restrict this these values may allow an attacker to spoof links generated by your service.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><description>Port numbers must be excluded.</description></item>
/// <item><description>A top level wildcard "*" allows all non-empty hosts.</description></item>
/// <item><description>Subdomain wildcards are permitted. E.g. "*.example.com" matches subdomains like foo.example.com,
/// but not the parent domain example.com.</description></item>
/// <item><description>Unicode host names are allowed but will be converted to punycode for matching.</description></item>
/// <item><description>IPv6 addresses must include their bounding brackets and be in their normalized form.</description></item>
/// </list>
/// </remarks>
public IList<string> AllowedHosts { get; set; } = new List<string>();
/// <summary>
/// Require the number of header values to be in sync between the different headers being processed.
/// The default is 'false'.

View File

@ -2,11 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{
@ -20,15 +18,13 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
/// <returns>The <see cref="IApplicationBuilder"/> for HttpsRedirection.</returns>
/// <remarks>
/// HTTPS Enforcement interanlly uses the UrlRewrite middleware to redirect HTTP requests to HTTPS.
/// </remarks>
public static IApplicationBuilder UseHttpsRedirection(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var serverAddressFeature = app.ServerFeatures.Get<IServerAddressesFeature>();
if (serverAddressFeature != null)
{

View File

@ -0,0 +1,183 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.HostFiltering
{
public class HostFilteringMiddlewareTests
{
[Fact]
public async Task MissingConfigThrows()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHostFiltering();
});
await Assert.ThrowsAsync<InvalidOperationException>(() => new TestServer(builder).SendAsync(_ => { }));
}
[Theory]
[InlineData(true, 200)]
[InlineData(false, 400)]
public async Task AllowsMissingHost(bool allowed, int status)
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHostFiltering(options =>
{
options.AllowEmptyHosts = allowed;
options.AllowedHosts.Add("Localhost");
});
})
.Configure(app =>
{
app.Use((ctx, next) =>
{
ctx.Request.Headers.Remove(HeaderNames.Host);
return next();
});
app.UseHostFiltering();
app.Run(c =>
{
Assert.False(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host));
return Task.CompletedTask;
});
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync("/");
Assert.Equal(status, (int)response.StatusCode);
}
[Theory]
[InlineData(true, 200)]
[InlineData(false, 400)]
public async Task AllowsEmptyHost(bool allowed, int status)
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHostFiltering(options =>
{
options.AllowEmptyHosts = allowed;
options.AllowedHosts.Add("Localhost");
});
})
.Configure(app =>
{
app.Use((ctx, next) =>
{
ctx.Request.Headers[HeaderNames.Host] = " ";
return next();
});
app.UseHostFiltering();
app.Run(c =>
{
Assert.True(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host));
Assert.True(StringValues.Equals(" ", host));
return Task.CompletedTask;
});
app.Run(c => Task.CompletedTask);
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync("/");
Assert.Equal(status, (int)response.StatusCode);
}
[Theory]
[InlineData("localHost", "localhost")]
[InlineData("localHost", "*")] // Any - Used by HttpSys
[InlineData("localHost", "[::]")] // IPv6 Any - This is what Kestrel reports when binding to *
[InlineData("localHost", "0.0.0.0")] // IPv4 Any
[InlineData("localhost:9090", "example.com;localHost")]
[InlineData("example.com:443", "example.com;localhost")]
[InlineData("localHost:80", "localhost;")]
[InlineData("foo.eXample.com:443", "*.exampLe.com")]
[InlineData("f.eXample.com:443", "*.exampLe.com")]
[InlineData("127.0.0.1", "127.0.0.1")]
[InlineData("127.0.0.1:443", "127.0.0.1")]
[InlineData("xn--c1yn36f:443", "xn--c1yn36f")]
[InlineData("xn--c1yn36f:443", "點看")]
[InlineData("[::ABC]", "[::aBc]")]
[InlineData("[::1]:80", "[::1]")]
public async Task AllowsSpecifiedHost(string host, string allowedHost)
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHostFiltering(options =>
{
options.AllowedHosts = allowedHost.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
});
})
.Configure(app =>
{
app.Use((ctx, next) =>
{
// TestHost's ClientHandler doesn't let you set the host header, only the host in the URI
// and that would over-normalize some of our test conditions like casing.
ctx.Request.Headers[HeaderNames.Host] = host;
return next();
});
app.UseHostFiltering();
app.Run(c => Task.CompletedTask);
});
var server = new TestServer(builder);
var response = await server.CreateRequest("/").GetAsync();
Assert.Equal(200, (int)response.StatusCode);
}
[Theory]
[InlineData("example.com", "localhost")]
[InlineData("localhost:9090", "example.com;")]
[InlineData(";", "example.com;localhost")]
[InlineData(";:80", "example.com;localhost")]
[InlineData(":80", "localhost")]
[InlineData(":", "localhost")]
[InlineData("example.com:443", "*.example.com")]
[InlineData(".example.com:443", "*.example.com")]
[InlineData("foo.com:443", "*.example.com")]
[InlineData("foo.example.com.bar:443", "*.example.com")]
[InlineData(".com:443", "*.com")]
// Unicode in the host shouldn't be allowed without punycode anyways. This match fails because the middleware converts
// its input to punycode.
[InlineData("點看", "點看")]
[InlineData("[::1", "[::1]")]
[InlineData("[::1:80", "[::1]")]
public async Task RejectsMismatchedHosts(string host, string allowedHost)
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHostFiltering(options =>
{
options.AllowedHosts = allowedHost.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
});
})
.Configure(app =>
{
app.Use((ctx, next) =>
{
// TestHost's ClientHandler doesn't let you set the host header, only the host in the URI
// and that would reject some of our test conditions.
ctx.Request.Headers[HeaderNames.Host] = host;
return next();
});
app.UseHostFiltering();
app.Run(c => throw new NotImplementedException("App"));
});
var server = new TestServer(builder);
var response = await server.CreateRequest("/").GetAsync();
Assert.Equal(400, (int)response.StatusCode);
}
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.HostFiltering\Microsoft.AspNetCore.HostFiltering.csproj" />
</ItemGroup>
</Project>

View File

@ -1,12 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.HttpOverrides
@ -392,6 +394,119 @@ namespace Microsoft.AspNetCore.HttpOverrides
Assert.True(assertsExecuted);
}
[Theory]
[InlineData("localHost", "localhost")]
[InlineData("localHost", "*")] // Any - Used by HttpSys
[InlineData("localHost", "[::]")] // IPv6 Any - This is what Kestrel reports when binding to *
[InlineData("localHost", "0.0.0.0")] // IPv4 Any
[InlineData("localhost:9090", "example.com;localHost")]
[InlineData("example.com:443", "example.com;localhost")]
[InlineData("localHost:80", "localhost;")]
[InlineData("foo.eXample.com:443", "*.exampLe.com")]
[InlineData("f.eXample.com:443", "*.exampLe.com")]
[InlineData("127.0.0.1", "127.0.0.1")]
[InlineData("127.0.0.1:443", "127.0.0.1")]
[InlineData("xn--c1yn36f:443", "xn--c1yn36f")]
[InlineData("xn--c1yn36f:443", "點看")]
[InlineData("[::ABC]", "[::aBc]")]
[InlineData("[::1]:80", "[::1]")]
public async Task XForwardedHostAllowsSpecifiedHost(string host, string allowedHost)
{
bool assertsExecuted = false;
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedHost,
AllowedHosts = allowedHost.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
});
app.Run(context =>
{
Assert.Equal(host, context.Request.Headers[HeaderNames.Host]);
assertsExecuted = true;
return Task.FromResult(0);
});
});
var server = new TestServer(builder);
var response = await server.SendAsync(ctx =>
{
ctx.Request.Headers["X-forwarded-Host"] = host;
});
Assert.True(assertsExecuted);
}
[Theory]
[InlineData("example.com", "localhost")]
[InlineData("localhost:9090", "example.com;")]
[InlineData(";", "example.com;localhost")]
[InlineData(";:80", "example.com;localhost")]
[InlineData(":80", "localhost")]
[InlineData(":", "localhost")]
[InlineData("example.com:443", "*.example.com")]
[InlineData(".example.com:443", "*.example.com")]
[InlineData("foo.com:443", "*.example.com")]
[InlineData("foo.example.com.bar:443", "*.example.com")]
[InlineData(".com:443", "*.com")]
// Unicode in the host shouldn't be allowed without punycode anyways. This match fails because the middleware converts
// its input to punycode.
[InlineData("點看", "點看")]
[InlineData("[::1", "[::1]")]
[InlineData("[::1:80", "[::1]")]
public async Task XForwardedHostFailsMismatchedHosts(string host, string allowedHost)
{
bool assertsExecuted = false;
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedHost,
AllowedHosts = new[] { allowedHost }
});
app.Run(context =>
{
Assert.NotEqual<string>(host, context.Request.Headers[HeaderNames.Host]);
assertsExecuted = true;
return Task.FromResult(0);
});
});
var server = new TestServer(builder);
var response = await server.SendAsync(ctx =>
{
ctx.Request.Headers["X-forwarded-Host"] = host;
});
Assert.True(assertsExecuted);
}
[Fact]
public async Task XForwardedHostStopsAtFirstUnspecifiedHost()
{
bool assertsExecuted = false;
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedHost,
ForwardLimit = 10,
AllowedHosts = new[] { "bar.com", "*.foo.com" }
});
app.Run(context =>
{
Assert.Equal("bar.foo.com:432", context.Request.Headers[HeaderNames.Host]);
assertsExecuted = true;
return Task.FromResult(0);
});
});
var server = new TestServer(builder);
var response = await server.SendAsync(ctx =>
{
ctx.Request.Headers["X-forwarded-Host"] = "stuff:523, bar.foo.com:432, bar.com:80";
});
Assert.True(assertsExecuted);
}
[Theory]
[InlineData(0, "h1", "http")]
[InlineData(1, "", "http")]

View File

@ -22,9 +22,6 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
public async Task SetOptions_DefaultsSetCorrectly()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
})
.Configure(app =>
{
app.UseHttpsRedirection();
@ -34,9 +31,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
});
});
var featureCollection = new FeatureCollection();
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
var server = new TestServer(builder, featureCollection);
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
@ -75,9 +70,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
});
});
var featureCollection = new FeatureCollection();
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
var server = new TestServer(builder, featureCollection);
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
@ -115,9 +108,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
});
});
var featureCollection = new FeatureCollection();
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
var server = new TestServer(builder, featureCollection);
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
@ -182,12 +173,6 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
public async Task SetServerAddressesFeature_SingleHttpsAddress_Success()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHttpsRedirection(options =>
{
});
})
.Configure(app =>
{
app.UseHttpsRedirection();
@ -215,12 +200,6 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
public async Task SetServerAddressesFeature_MultipleHttpsAddresses_ThrowInMiddleware()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHttpsRedirection(options =>
{
});
})
.Configure(app =>
{
app.UseHttpsRedirection();
@ -248,12 +227,6 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
public async Task SetServerAddressesFeature_MultipleHttpsAddressesWithSamePort_Success()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHttpsRedirection(options =>
{
});
})
.Configure(app =>
{
app.UseHttpsRedirection();