Skip to content

Commit 4fbc6ff

Browse files
BrennanConroycaptainsafia
authored andcommitted
Fix Kestrel host header mismatch handling when port in Url (#59352)
1 parent 413087b commit 4fbc6ff

File tree

3 files changed

+25
-10
lines changed

3 files changed

+25
-10
lines changed

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Buffers;
55
using System.Diagnostics;
6-
using System.Globalization;
76
using System.IO.Pipelines;
87
using Microsoft.AspNetCore.Connections;
98
using Microsoft.AspNetCore.Connections.Features;
@@ -644,16 +643,24 @@ private void ValidateNonOriginHostHeader(string hostText)
644643
// authority component, excluding any userinfo subcomponent and its "@"
645644
// delimiter.
646645

646+
// Accessing authority always allocates, store it in a local to only allocate once
647+
var authority = _absoluteRequestTarget!.Authority;
648+
647649
// System.Uri doesn't not tell us if the port was in the original string or not.
648650
// When IsDefaultPort = true, we will allow Host: with or without the default port
649-
if (hostText != _absoluteRequestTarget!.Authority)
651+
if (hostText != authority)
650652
{
651653
if (!_absoluteRequestTarget.IsDefaultPort
652-
|| hostText != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture))
654+
|| hostText != $"{authority}:{_absoluteRequestTarget.Port}")
653655
{
654656
if (_context.ServiceContext.ServerOptions.AllowHostHeaderOverride)
655657
{
656-
hostText = _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture);
658+
// No need to include the port here, it's either already in the Authority
659+
// or it's the default port
660+
// see: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.23
661+
// A "host" without any trailing port information implies the default
662+
// port for the service requested (e.g., "80" for an HTTP URL).
663+
hostText = authority;
657664
HttpRequestHeaders.HeaderHost = hostText;
658665
}
659666
else

src/Servers/Kestrel/shared/test/HttpParsingData.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -497,8 +497,10 @@ public static TheoryData<string, string> HostHeaderData
497497
{ "GET /pub/WWW/", "www.example.org" },
498498
{ "GET http://localhost/", "localhost" },
499499
{ "GET http://localhost:80/", "localhost:80" },
500+
{ "GET http://localhost:80/", "localhost" },
500501
{ "GET https://localhost/", "localhost" },
501502
{ "GET https://localhost:443/", "localhost:443" },
503+
{ "GET https://localhost:443/", "localhost" },
502504
{ "CONNECT asp.net:80", "asp.net:80" },
503505
{ "CONNECT asp.net:443", "asp.net:443" },
504506
{ "CONNECT user-images.githubusercontent.com:443", "user-images.githubusercontent.com:443" },
@@ -534,10 +536,13 @@ public static TheoryData<string, string> HostHeaderInvalidData
534536
data.Add("CONNECT contoso.com", host);
535537
}
536538

537-
// port mismatch when target contains port
539+
// port mismatch when target contains default https port
538540
data.Add("GET https://contoso.com:443/", "contoso.com:5000");
539541
data.Add("CONNECT contoso.com:443", "contoso.com:5000");
540542

543+
// port mismatch when target contains default http port
544+
data.Add("GET http://contoso.com:80/", "contoso.com:5000");
545+
541546
return data;
542547
}
543548
}

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

+8-5
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,12 @@ public Task BadRequestIfHostHeaderDoesNotMatchRequestTarget(string requestTarget
153153
}
154154

155155
[Theory]
156-
[InlineData("Host: www.foo.comConnection: keep-alive")] // Corrupted - missing line-break
157-
[InlineData("Host: www.notfoo.com")] // Syntactically correct but not matching
158-
public async Task CanOptOutOfBadRequestIfHostHeaderDoesNotMatchRequestTarget(string hostHeader)
156+
[InlineData("http://www.foo.com", "Host: www.foo.comConnection: keep-alive", "www.foo.com")] // Corrupted - missing line-break
157+
[InlineData("http://www.foo.com/", "Host: www.notfoo.com", "www.foo.com")] // Syntactically correct but not matching
158+
[InlineData("http://www.foo.com:80", "Host: www.notfoo.com", "www.foo.com")] // Explicit default port in request string
159+
[InlineData("http://www.foo.com:5129", "Host: www.foo.com", "www.foo.com:5129")] // Non-default port in request string
160+
[InlineData("http://www.foo.com:5129", "Host: www.foo.com:5128", "www.foo.com:5129")] // Different port in host header
161+
public async Task CanOptOutOfBadRequestIfHostHeaderDoesNotMatchRequestTarget(string requestString, string hostHeader, string expectedHost)
159162
{
160163
var testMeterFactory = new TestMeterFactory();
161164
using var connectionDuration = new MetricCollector<double>(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration");
@@ -175,13 +178,13 @@ public async Task CanOptOutOfBadRequestIfHostHeaderDoesNotMatchRequestTarget(str
175178
{
176179
using (var client = server.CreateConnection())
177180
{
178-
await client.SendAll($"GET http://www.foo.com/api/data HTTP/1.1\r\n{hostHeader}\r\n\r\n");
181+
await client.SendAll($"GET {requestString} HTTP/1.1\r\n{hostHeader}\r\n\r\n");
179182

180183
await client.Receive("HTTP/1.1 200 OK");
181184
}
182185
}
183186

184-
Assert.Equal("www.foo.com:80", receivedHost);
187+
Assert.Equal(expectedHost, receivedHost);
185188

186189
Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags));
187190
}

0 commit comments

Comments
 (0)