diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 3a86d96e9a57..9de5568317a8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -626,7 +626,15 @@ private void ValidateNonOriginHostHeader(string hostText) if (!_absoluteRequestTarget.IsDefaultPort || hostText != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture)) { - KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); + if (_context.ServiceContext.ServerOptions.AllowHostHeaderOverride) + { + 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 b44e3b33adcf..c9e8db2f7a93 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -35,6 +35,29 @@ public class KestrelServerOptions private Func _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector; + /// + /// In HTTP/1.x, when a request target is in absolute-form (see RFC 9112 Section 3.2.2), + /// for example + /// + /// GET http://www.example.com/path/to/index.html HTTP/1.1 + /// + /// the Host header is redundant. In fact, the RFC says + /// + /// When an origin server receives a request with an absolute-form of request-target, + /// the origin server MUST ignore the received Host header field (if any) and instead + /// use the host information of the request-target. + /// + /// However, it is still sensible to check whether the request target and Host header match + /// because a mismatch might indicate, for example, a spoofing attempt. Setting this property + /// to true bypasses that check and unconditionally overwrites the Host header with the value + /// from the request target. + /// + /// + /// This option does not apply to HTTP/2 or HTTP/3. + /// + /// + public bool AllowHostHeaderOverride { get; set; } + // 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/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index 5a2c7ffb53b1..b9a42177f4ef 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature.SslStream.get -> System.Net.Security.SslStream! +Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowHostHeaderOverride.get -> bool +Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowHostHeaderOverride.set -> void Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName) -> void Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName, System.Action! configure) -> void Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string? \ No newline at end of file diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs index 5bde02517fff..e85fa8f0ba1d 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,32 @@ public Task BadRequestIfHostHeaderDoesNotMatchRequestTarget(string requestTarget CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(host.Trim())); } + [Theory] + [InlineData("Host: www.foo.comConnection: keep-alive")] // Corrupted - missing line-break + [InlineData("Host: www.notfoo.com")] // Syntactically correct but not matching + public async Task CanOptOutOfBadRequestIfHostHeaderDoesNotMatchRequestTarget(string hostHeader) + { + 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() + { + AllowHostHeaderOverride = true, + } + }); + using var client = server.CreateConnection(); + + await client.SendAll($"GET http://www.foo.com/api/data HTTP/1.1\r\n{hostHeader}\r\n\r\n"); + + await client.Receive("HTTP/1.1 200 OK"); + + Assert.Equal("www.foo.com:80", receivedHost); + } + [Fact] public Task BadRequestFor10BadHostHeaderFormat() {