Skip to content

Commit 2c553fd

Browse files
authored
Allow alternate schemes in Kestrel requests #30532 (#34013)
1 parent a3d1956 commit 2c553fd

File tree

6 files changed

+146
-25
lines changed

6 files changed

+146
-25
lines changed

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,17 +240,19 @@ private bool TryValidatePseudoHeaders()
240240
// ":scheme" is not restricted to "http" and "https" schemed URIs. A
241241
// proxy or gateway can translate requests for non - HTTP schemes,
242242
// enabling the use of HTTP to interact with non - HTTP services.
243-
244-
// - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right?
245-
// - For now we'll restrict it to http/s and require it match the transport.
246-
// - We'll need to find some concrete scenarios to warrant unblocking this.
243+
// A common example is TLS termination.
247244
var headerScheme = HttpRequestHeaders.HeaderScheme.ToString();
248245
if (!ReferenceEquals(headerScheme, Scheme) &&
249246
!string.Equals(headerScheme, Scheme, StringComparison.OrdinalIgnoreCase))
250247
{
251-
ResetAndAbort(new ConnectionAbortedException(
252-
CoreStrings.FormatHttp2StreamErrorSchemeMismatch(HttpRequestHeaders.HeaderScheme, Scheme)), Http2ErrorCode.PROTOCOL_ERROR);
253-
return false;
248+
if (!ServerOptions.AllowAlternateSchemes || !Uri.CheckSchemeName(headerScheme))
249+
{
250+
ResetAndAbort(new ConnectionAbortedException(
251+
CoreStrings.FormatHttp2StreamErrorSchemeMismatch(HttpRequestHeaders.HeaderScheme, Scheme)), Http2ErrorCode.PROTOCOL_ERROR);
252+
return false;
253+
}
254+
255+
Scheme = headerScheme;
254256
}
255257

256258
// :path (and query) - Required

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -685,15 +685,18 @@ private bool TryValidatePseudoHeaders()
685685
// ":scheme" is not restricted to "http" and "https" schemed URIs. A
686686
// proxy or gateway can translate requests for non - HTTP schemes,
687687
// enabling the use of HTTP to interact with non - HTTP services.
688-
689-
// - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right?
690-
// - For now we'll restrict it to http/s and require it match the transport.
691-
// - We'll need to find some concrete scenarios to warrant unblocking this.
692-
if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase))
688+
var headerScheme = HttpRequestHeaders.HeaderScheme.ToString();
689+
if (!ReferenceEquals(headerScheme, Scheme) &&
690+
!string.Equals(headerScheme, Scheme, StringComparison.OrdinalIgnoreCase))
693691
{
694-
var str = CoreStrings.FormatHttp3StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme);
695-
Abort(new ConnectionAbortedException(str), Http3ErrorCode.ProtocolError);
696-
return false;
692+
if (!ServerOptions.AllowAlternateSchemes || !Uri.CheckSchemeName(headerScheme))
693+
{
694+
var str = CoreStrings.FormatHttp3StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme);
695+
Abort(new ConnectionAbortedException(str), Http3ErrorCode.ProtocolError);
696+
return false;
697+
}
698+
699+
Scheme = headerScheme;
697700
}
698701

699702
// :path (and query) - Required

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ public class KestrelServerOptions
6868
/// </remarks>
6969
public bool AllowSynchronousIO { get; set; }
7070

71+
/// Gets or sets a value that controls how the `:scheme` field for HTTP/2 and HTTP/3 requests is validated.
72+
/// <para>
73+
/// If <c>false</c> then the `:scheme` field for HTTP/2 and HTTP/3 requests must exactly match the transport (e.g. https for TLS
74+
/// connections, http for non-TLS). If <c>true</c> then the `:scheme` field for HTTP/2 and HTTP/3 requests can be set to alternate values
75+
/// and this will be reflected by `HttpRequest.Scheme`. The Scheme must still be valid according to
76+
/// https://datatracker.ietf.org/doc/html/rfc3986/#section-3.1. Only enable this when working with a trusted proxy. This can be used in
77+
/// scenarios such as proxies converting from alternate protocols. See https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3.
78+
/// Applications that enable this should validate an expected scheme is provided before using it.
79+
/// </para>
80+
/// <remarks>
81+
/// Defaults to <c>false</c>.
82+
/// </remarks>
83+
public bool AllowAlternateSchemes { get; set; }
84+
7185
/// <summary>
7286
/// Gets or sets a value that controls whether the string values materialized
7387
/// will be reused across requests; if they match, or if the strings will always be reallocated.

src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
*REMOVED*~static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions, string fileName, string password) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions
9696
*REMOVED*~static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions, string fileName, string password, System.Action<Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions> configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions
9797
*REMOVED*~static Microsoft.AspNetCore.Server.Kestrel.Https.CertificateLoader.LoadFromStoreCert(string subject, string storeName, System.Security.Cryptography.X509Certificates.StoreLocation storeLocation, bool allowInvalid) -> System.Security.Cryptography.X509Certificates.X509Certificate2
98+
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowAlternateSchemes.get -> bool
99+
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowAlternateSchemes.set -> void
98100
Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.DelayCertificate = 3 -> Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode
99101
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.get -> System.Func<string!, System.Text.Encoding?>!
100102
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.set -> void

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,19 +404,73 @@ public async Task HEADERS_Received_CONNECTMethod_WithSchemeOrPath_Reset(string h
404404
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
405405
}
406406

407-
[Fact]
408-
public async Task HEADERS_Received_SchemeMismatch_Reset()
407+
[Theory]
408+
[InlineData("https")]
409+
[InlineData("ftp")]
410+
public async Task HEADERS_Received_SchemeMismatch_Reset(string scheme)
409411
{
410412
await InitializeConnectionAsync(_noopApplication);
411413

412-
// :path and :scheme are not allowed, :authority is optional
413414
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
414415
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
415-
new KeyValuePair<string, string>(HeaderNames.Scheme, "https") }; // Not the expected "http"
416+
new KeyValuePair<string, string>(HeaderNames.Scheme, scheme) }; // Not the expected "http"
417+
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
418+
419+
await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR,
420+
CoreStrings.FormatHttp2StreamErrorSchemeMismatch(scheme, "http"));
421+
422+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
423+
}
424+
425+
[Theory]
426+
[InlineData("https")]
427+
[InlineData("ftp")]
428+
public async Task HEADERS_Received_SchemeMismatchAllowed_Processed(string scheme)
429+
{
430+
_serviceContext.ServerOptions.AllowAlternateSchemes = true;
431+
432+
await InitializeConnectionAsync(context =>
433+
{
434+
Assert.Equal(scheme, context.Request.Scheme);
435+
return Task.CompletedTask;
436+
});
437+
438+
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
439+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
440+
new KeyValuePair<string, string>(HeaderNames.Scheme, scheme) }; // Not the expected "http"
441+
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
442+
443+
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
444+
withLength: 36,
445+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
446+
withStreamId: 1);
447+
448+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
449+
450+
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
451+
452+
Assert.Equal(3, _decodedHeaders.Count);
453+
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
454+
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
455+
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
456+
}
457+
458+
[Theory]
459+
[InlineData("https,http")]
460+
[InlineData("http://fakehost/")]
461+
public async Task HEADERS_Received_SchemeMismatchAllowed_InvalidScheme_Reset(string scheme)
462+
{
463+
_serviceContext.ServerOptions.AllowAlternateSchemes = true;
464+
465+
await InitializeConnectionAsync(_noopApplication);
466+
467+
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
468+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
469+
new KeyValuePair<string, string>(HeaderNames.Scheme, scheme) }; // Not the expected "http"
416470
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
417471

418472
await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR,
419-
CoreStrings.FormatHttp2StreamErrorSchemeMismatch("https", "http"));
473+
CoreStrings.FormatHttp2StreamErrorSchemeMismatch(scheme, "http"));
420474

421475
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
422476
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,19 +261,65 @@ public async Task ConnectMethod_WithSchemeOrPath_Reset(string headerName, string
261261
await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.Http3ErrorConnectMustNotSendSchemeOrPath);
262262
}
263263

264-
[Fact]
265-
public async Task SchemeMismatch_Reset()
264+
[Theory]
265+
[InlineData("https")]
266+
[InlineData("ftp")]
267+
public async Task SchemeMismatch_Reset(string scheme)
266268
{
267269
var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication);
268270

269-
// :path and :scheme are not allowed, :authority is optional
270271
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
271272
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
272-
new KeyValuePair<string, string>(HeaderNames.Scheme, "https") }; // Not the expected "http"
273+
new KeyValuePair<string, string>(HeaderNames.Scheme, scheme) }; // Not the expected "http"
274+
275+
await requestStream.SendHeadersAsync(headers, endStream: true);
276+
277+
await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3StreamErrorSchemeMismatch(scheme, "http"));
278+
}
279+
280+
[Theory]
281+
[InlineData("https")]
282+
[InlineData("ftp")]
283+
public async Task SchemeMismatchAllowed_Processed(string scheme)
284+
{
285+
_serviceContext.ServerOptions.AllowAlternateSchemes = true;
286+
287+
var requestStream = await InitializeConnectionAndStreamsAsync(context =>
288+
{
289+
Assert.Equal(scheme, context.Request.Scheme);
290+
return Task.CompletedTask;
291+
});
292+
293+
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
294+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
295+
new KeyValuePair<string, string>(HeaderNames.Scheme, scheme) }; // Not the expected "http"
296+
297+
await requestStream.SendHeadersAsync(headers, endStream: true);
298+
299+
var responseHeaders = await requestStream.ExpectHeadersAsync();
300+
301+
Assert.Equal(3, responseHeaders.Count);
302+
Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase);
303+
Assert.Equal("200", responseHeaders[HeaderNames.Status]);
304+
Assert.Equal("0", responseHeaders["content-length"]);
305+
}
306+
307+
[Theory]
308+
[InlineData("https,http")]
309+
[InlineData("http://fakehost/")]
310+
public async Task SchemeMismatchAllowed_InvalidScheme_Reset(string scheme)
311+
{
312+
_serviceContext.ServerOptions.AllowAlternateSchemes = true;
313+
314+
var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication);
315+
316+
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
317+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
318+
new KeyValuePair<string, string>(HeaderNames.Scheme, scheme) }; // Not the expected "http"
273319

274320
await requestStream.SendHeadersAsync(headers, endStream: true);
275321

276-
await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3StreamErrorSchemeMismatch("https", "http"));
322+
await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3StreamErrorSchemeMismatch(scheme, "http"));
277323
}
278324

279325
[Fact]

0 commit comments

Comments
 (0)