();
+
+ return app;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.HostFiltering/HostFilteringMiddleware.cs b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringMiddleware.cs
new file mode 100644
index 0000000000..e1e38eca37
--- /dev/null
+++ b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringMiddleware.cs
@@ -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
+{
+ ///
+ /// A middleware used to filter requests by their Host header.
+ ///
+ public class HostFilteringMiddleware
+ {
+ // Matches Http.Sys.
+ private static readonly byte[] DefaultResponse = Encoding.ASCII.GetBytes(
+ "\r\n"
+ + "Bad Request\r\n"
+ + " HEAD >\r\n"
+ + "Bad Request - Invalid Hostname
\r\n"
+ + "
HTTP Error 400. The request hostname is invalid.
\r\n"
+ + "");
+
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+ private readonly HostFilteringOptions _options;
+ private IList _allowedHosts;
+ private bool? _allowAnyNonEmptyHost;
+
+ ///
+ /// A middleware used to filter requests by their Host header.
+ ///
+ ///
+ ///
+ ///
+ public HostFilteringMiddleware(RequestDelegate next, ILogger logger,
+ IOptions options)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ ///
+ /// Processes requests
+ ///
+ ///
+ ///
+ 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();
+ 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 incoming, IList 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.HostFiltering/HostFilteringOptions.cs b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringOptions.cs
new file mode 100644
index 0000000000..1645591579
--- /dev/null
+++ b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringOptions.cs
@@ -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
+{
+ ///
+ /// Options for the HostFiltering middleware
+ ///
+ public class HostFilteringOptions
+ {
+ ///
+ /// The hosts headers that are allowed to access this site. At least one value is required.
+ ///
+ ///
+ ///
+ /// - Port numbers must be excluded.
+ /// - A top level wildcard "*" allows all non-empty hosts.
+ /// - Subdomain wildcards are permitted. E.g. "*.example.com" matches subdomains like foo.example.com,
+ /// but not the parent domain example.com.
+ /// - Unicode host names are allowed but will be converted to punycode for matching.
+ /// - IPv6 addresses must include their bounding brackets and be in their normalized form.
+ ///
+ ///
+ public IList AllowedHosts { get; set; } = new List();
+
+ ///
+ /// Indicates if requests without hosts are allowed. The default is true.
+ ///
+ ///
+ /// HTTP/1.0 does not require a host header.
+ /// Http/1.1 requires a host header, but says the value may be empty.
+ ///
+ 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.
+ ///
+ /// Indicates if the 400 response should include a default message or be empty. This is enabled by default.
+ ///
+ public bool IncludeFailureMessage { get; set; } = true;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.HostFiltering/HostFilteringServicesExtensions.cs b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringServicesExtensions.cs
new file mode 100644
index 0000000000..81b48444a8
--- /dev/null
+++ b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringServicesExtensions.cs
@@ -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
+{
+ ///
+ /// Extension methods for the host filtering middleware.
+ ///
+ public static class HostFilteringServicesExtensions
+ {
+ ///
+ /// Adds services and options for the host filtering middleware.
+ ///
+ /// The for adding services.
+ /// A delegate to configure the .
+ ///
+ public static IServiceCollection AddHostFiltering(this IServiceCollection services, Action configureOptions)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+ if (configureOptions == null)
+ {
+ throw new ArgumentNullException(nameof(configureOptions));
+ }
+
+ services.Configure(configureOptions);
+ return services;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.HostFiltering/Microsoft.AspNetCore.HostFiltering.csproj b/src/Microsoft.AspNetCore.HostFiltering/Microsoft.AspNetCore.HostFiltering.csproj
new file mode 100644
index 0000000000..ec6d642340
--- /dev/null
+++ b/src/Microsoft.AspNetCore.HostFiltering/Microsoft.AspNetCore.HostFiltering.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+ ASP.NET Core middleware for filtering out requests with unknown HTTP host headers.
+
+ netstandard2.0
+ true
+ aspnetcore
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
index d5e074de57..decda81d58 100644
--- a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
+++ b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
@@ -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 _allowedHosts;
static ForwardedHeadersMiddleware()
{
@@ -85,6 +89,8 @@ namespace Microsoft.AspNetCore.HttpOverrides
_options = options.Value;
_logger = loggerFactory.CreateLogger();
_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();
+ 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;
diff --git a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersOptions.cs b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersOptions.cs
index cbe00baaa1..3507d9bea5 100644
--- a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersOptions.cs
+++ b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersOptions.cs
@@ -67,6 +67,22 @@ namespace Microsoft.AspNetCore.Builder
///
public IList KnownNetworks { get; } = new List() { new IPNetwork(IPAddress.Loopback, 8) };
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ /// - Port numbers must be excluded.
+ /// - A top level wildcard "*" allows all non-empty hosts.
+ /// - Subdomain wildcards are permitted. E.g. "*.example.com" matches subdomains like foo.example.com,
+ /// but not the parent domain example.com.
+ /// - Unicode host names are allowed but will be converted to punycode for matching.
+ /// - IPv6 addresses must include their bounding brackets and be in their normalized form.
+ ///
+ ///
+ public IList AllowedHosts { get; set; } = new List();
+
///
/// Require the number of header values to be in sync between the different headers being processed.
/// The default is 'false'.
diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs
index 429f0f0ee2..c6f804763f 100644
--- a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs
+++ b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs
@@ -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
///
/// The instance this method extends.
/// The for HttpsRedirection.
- ///
- /// HTTPS Enforcement interanlly uses the UrlRewrite middleware to redirect HTTP requests to HTTPS.
- ///
public static IApplicationBuilder UseHttpsRedirection(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
+
var serverAddressFeature = app.ServerFeatures.Get();
if (serverAddressFeature != null)
{
diff --git a/test/Microsoft.AspNetCore.HostFiltering.Tests/HostFilteringMiddlewareTests.cs b/test/Microsoft.AspNetCore.HostFiltering.Tests/HostFilteringMiddlewareTests.cs
new file mode 100644
index 0000000000..2fc7b8c713
--- /dev/null
+++ b/test/Microsoft.AspNetCore.HostFiltering.Tests/HostFilteringMiddlewareTests.cs
@@ -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(() => 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);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.HostFiltering.Tests/Microsoft.AspNetCore.HostFiltering.Tests.csproj b/test/Microsoft.AspNetCore.HostFiltering.Tests/Microsoft.AspNetCore.HostFiltering.Tests.csproj
new file mode 100644
index 0000000000..d71805c949
--- /dev/null
+++ b/test/Microsoft.AspNetCore.HostFiltering.Tests/Microsoft.AspNetCore.HostFiltering.Tests.csproj
@@ -0,0 +1,11 @@
+
+
+
+ $(StandardTestTfms)
+
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs b/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
index 81d6ccf15a..f88243c9e7 100644
--- a/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
+++ b/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
@@ -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(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")]
diff --git a/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs
index 9d482151ca..b55c195272 100644
--- a/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs
+++ b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs
@@ -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(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(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(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();