Skip to content

Commit c52c200

Browse files
amcaseyTratcherhalter73
authored
Introduce KestrelServerOptions.AllowUnsafeHostHeaderOverride (#48460)
* Allow overriding the host header if doesn't match the absolute-form host (#39334) * Allow overriding the host header if doesn't match the absolute-form host * Apply suggestions from code review Co-authored-by: Stephen Halter <[email protected]> * Add explanatory comment * Replace internal API and appcontext switch with public API The new public API is `KestrelServerOptions.AllowUnsafeHostHeaderOverride` and I've moved the explanatory comments there. The behavior remains opt-in. * Separate corruption and mismatch tests * Rename property per API review * Clarify comment. Co-authored-by: Chris Ross <[email protected]> --------- Co-authored-by: Chris Ross <[email protected]> Co-authored-by: Stephen Halter <[email protected]> Co-authored-by: Chris Ross <[email protected]>
1 parent a020933 commit c52c200

File tree

4 files changed

+61
-1
lines changed

4 files changed

+61
-1
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,15 @@ private void ValidateNonOriginHostHeader(string hostText)
626626
if (!_absoluteRequestTarget.IsDefaultPort
627627
|| hostText != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture))
628628
{
629-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
629+
if (_context.ServiceContext.ServerOptions.AllowHostHeaderOverride)
630+
{
631+
hostText = _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture);
632+
HttpRequestHeaders.HeaderHost = hostText;
633+
}
634+
else
635+
{
636+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
637+
}
630638
}
631639
}
632640
}

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,29 @@ public class KestrelServerOptions
3535

3636
private Func<string, Encoding?> _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector;
3737

38+
/// <summary>
39+
/// In HTTP/1.x, when a request target is in absolute-form (see RFC 9112 Section 3.2.2),
40+
/// for example
41+
/// <code>
42+
/// GET http://www.example.com/path/to/index.html HTTP/1.1
43+
/// </code>
44+
/// the Host header is redundant. In fact, the RFC says
45+
///
46+
/// When an origin server receives a request with an absolute-form of request-target,
47+
/// the origin server MUST ignore the received Host header field (if any) and instead
48+
/// use the host information of the request-target.
49+
///
50+
/// However, it is still sensible to check whether the request target and Host header match
51+
/// because a mismatch might indicate, for example, a spoofing attempt. Setting this property
52+
/// to true bypasses that check and unconditionally overwrites the Host header with the value
53+
/// from the request target.
54+
/// </summary>
55+
/// <remarks>
56+
/// This option does not apply to HTTP/2 or HTTP/3.
57+
/// </remarks>
58+
/// <seealso href="https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.2-8"/>
59+
public bool AllowHostHeaderOverride { get; set; }
60+
3861
// 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.
3962
internal List<ListenOptions> CodeBackedListenOptions { get; } = new List<ListenOptions>();
4063
internal List<ListenOptions> ConfigurationBackedListenOptions { get; } = new List<ListenOptions>();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#nullable enable
22
Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature
33
Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature.SslStream.get -> System.Net.Security.SslStream!
4+
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowHostHeaderOverride.get -> bool
5+
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowHostHeaderOverride.set -> void
46
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName) -> void
57
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName, System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!>! configure) -> void
68
Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string?

src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.AspNetCore.Testing;
1212
using Microsoft.AspNetCore.WebUtilities;
1313
using Microsoft.Extensions.Logging;
14+
using Microsoft.Extensions.Primitives;
1415
using Moq;
1516
using Xunit;
1617
using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException;
@@ -140,6 +141,32 @@ public Task BadRequestIfHostHeaderDoesNotMatchRequestTarget(string requestTarget
140141
CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(host.Trim()));
141142
}
142143

144+
[Theory]
145+
[InlineData("Host: www.foo.comConnection: keep-alive")] // Corrupted - missing line-break
146+
[InlineData("Host: www.notfoo.com")] // Syntactically correct but not matching
147+
public async Task CanOptOutOfBadRequestIfHostHeaderDoesNotMatchRequestTarget(string hostHeader)
148+
{
149+
var receivedHost = StringValues.Empty;
150+
await using var server = new TestServer(context =>
151+
{
152+
receivedHost = context.Request.Headers.Host;
153+
return Task.CompletedTask;
154+
}, new TestServiceContext(LoggerFactory)
155+
{
156+
ServerOptions = new KestrelServerOptions()
157+
{
158+
AllowHostHeaderOverride = true,
159+
}
160+
});
161+
using var client = server.CreateConnection();
162+
163+
await client.SendAll($"GET http://www.foo.com/api/data HTTP/1.1\r\n{hostHeader}\r\n\r\n");
164+
165+
await client.Receive("HTTP/1.1 200 OK");
166+
167+
Assert.Equal("www.foo.com:80", receivedHost);
168+
}
169+
143170
[Fact]
144171
public Task BadRequestFor10BadHostHeaderFormat()
145172
{

0 commit comments

Comments
 (0)