diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 87998e408288..38e204bdf95c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -629,7 +629,25 @@ private void ValidateNonOriginHostHeader(string hostText) if (!_absoluteRequestTarget.IsDefaultPort || hostText != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture)) { - KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); + // Superseded by RFC 7230, but notable for back-compat. + // https://datatracker.ietf.org/doc/html/rfc2616/#section-5.2 + // 1. If Request-URI is an absoluteURI, the host is part of the + // Request-URI. Any Host header field value in the request MUST be + // ignored. + // We don't want to leave the invalid value for the app to accidentally consume, + // replace it with the value from the request line. + // This is insecure in the sense that we would ordinarily regard a mismatched + // request target and host as suspicious (e.g. indicative of a spoofing attempt) + // and reject the request. + if (_context.ServiceContext.ServerOptions.EnableInsecureAbsoluteFormHostOverride) + { + hostText = _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture); + HttpRequestHeaders.HeaderHost = hostText; + } + else + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); + } } } } diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index ba5c6151b4eb..ef3ed4d95495 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -34,6 +34,21 @@ public class KestrelServerOptions private Func _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector; + private bool? _enableInsecureAbsoluteFormHostOverride; + internal bool EnableInsecureAbsoluteFormHostOverride + { + get + { + if (!_enableInsecureAbsoluteFormHostOverride.HasValue) + { + _enableInsecureAbsoluteFormHostOverride = + AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.Kestrel.EnableInsecureAbsoluteFormHostOverride", out var enabled) && enabled; + } + return _enableInsecureAbsoluteFormHostOverride.Value; + } + set => _enableInsecureAbsoluteFormHostOverride = value; + } + // The following two lists configure the endpoints that Kestrel should listen to. If both lists are empty, the "urls" config setting (e.g. UseUrls) is used. internal List CodeBackedListenOptions { get; } = new List(); internal List ConfigurationBackedListenOptions { get; } = new List(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs index 5bde02517fff..308378ef7330 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Moq; using Xunit; using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; @@ -140,6 +141,30 @@ public Task BadRequestIfHostHeaderDoesNotMatchRequestTarget(string requestTarget CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(host.Trim())); } + [Fact] + public async Task CanOptOutOfBadRequestIfHostHeaderDoesNotMatchRequestTarget() + { + var receivedHost = StringValues.Empty; + await using var server = new TestServer(context => + { + receivedHost = context.Request.Headers.Host; + return Task.CompletedTask; + }, new TestServiceContext(LoggerFactory) + { + ServerOptions = new KestrelServerOptions() + { + EnableInsecureAbsoluteFormHostOverride = true, + } + }); + using var client = server.CreateConnection(); + + await client.SendAll($"GET http://www.foo.com/api/data HTTP/1.1\r\nHost: www.foo.comConnection: keep-alive\r\n\r\n"); + + await client.Receive("HTTP/1.1 200 OK"); + + Assert.Equal("www.foo.com:80", receivedHost); + } + [Fact] public Task BadRequestFor10BadHostHeaderFormat() {