From fa29d499c70ae2c3f80800e6ba841de754ddc03f Mon Sep 17 00:00:00 2001 From: Chris R Date: Thu, 20 Oct 2022 15:21:58 -0700 Subject: [PATCH 1/5] Send 431 when HTTP/2 headers are too large or many #17861 --- .../Core/src/Internal/Http/HttpProtocol.cs | 6 ++ .../src/Internal/Http2/Http2Connection.cs | 5 +- .../Core/src/Internal/Http2/Http2Stream.cs | 23 ++++++ .../Http2/Http2ConnectionTests.cs | 14 +++- .../Http2/Http2StreamTests.cs | 72 +++++++++++++++++++ .../HttpClientHttp2InteropTests.cs | 6 +- 6 files changed, 119 insertions(+), 7 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 8aa61f383167..a2befc02747b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -107,6 +107,7 @@ public void Initialize(HttpConnectionContext context) public long? MaxRequestBodySize { get; set; } public MinDataRate? MinRequestBodyDataRate { get; set; } public bool AllowSynchronousIO { get; set; } + protected int RequestHeadersParsed => _requestHeadersParsed; /// /// The request id. @@ -546,6 +547,11 @@ public void OnTrailer(ReadOnlySpan name, ReadOnlySpan value) private void IncrementRequestHeadersCount() { _requestHeadersParsed++; + CheckRequestHeadersCountLimit(); + } + + protected virtual void CheckRequestHeadersCountLimit(bool final = false) + { if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 668bfe8faffa..7927bbaddad8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1130,6 +1130,8 @@ private void StartStream() try { + _currentHeadersStream.TotalParsedHeaderSize = _totalParsedHeaderSize; + // This must be initialized before we offload the request or else we may start processing request body frames without it. _currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength; @@ -1413,7 +1415,8 @@ private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnly // https://tools.ietf.org/html/rfc7540#section-6.5.2 // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field."; _totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length; - if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize) + // Allow a 2x grace before aborting the connection. We'll check the size limit again later where we can send a 431. + if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2) { throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 6df8039ae77e..0b5eb2c66d3b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -25,6 +25,8 @@ internal abstract partial class Http2Stream : HttpProtocol, IThreadPoolWorkItem, private bool _decrementCalled; + public int TotalParsedHeaderSize { get; set; } + public Pipe RequestBodyPipe { get; private set; } = default!; internal long DrainExpirationTicks { get; set; } @@ -41,6 +43,7 @@ public void Initialize(Http2StreamContext context) InputRemaining = null; RequestBodyStarted = false; DrainExpirationTicks = 0; + TotalParsedHeaderSize = 0; _context = context; @@ -198,6 +201,15 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio // do the reading from a pipeline, nor do we use endConnection to report connection-level errors. endConnection = !TryValidatePseudoHeaders(); + // 431 if the headers are too large + if (TotalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize); + } + + // 431 if we received too many headers + CheckRequestHeadersCountLimit(final: true); + // Suppress pseudo headers from the public headers collection. HttpRequestHeaders.ClearPseudoRequestHeaders(); @@ -695,6 +707,17 @@ public override void OnHeader(int index, bool indexOnly, ReadOnlySpan name } } + // Final: Has the message been fully received? We allow up to 2x grace while receiving the message + // to avoid faulting the entire connection. We check again later when we can reject the request with a 431. + protected override void CheckRequestHeadersCountLimit(bool final = false) + { + if (final && RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount + || RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount * 2) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] private void AppendHeader(ReadOnlySpan name, ReadOnlySpan value) { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 89f6de2cc0fe..61f5bda90ef4 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -2724,7 +2724,7 @@ await WaitForConnectionErrorAsync( [Fact] public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError() { - // > 32kb + // > 32kb * 2 to exceed graceful handling limit var headers = new[] { new KeyValuePair(InternalHeaderNames.Method, "GET"), @@ -2738,6 +2738,14 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError() new KeyValuePair("f", _4kHeaderValue), new KeyValuePair("g", _4kHeaderValue), new KeyValuePair("h", _4kHeaderValue), + new KeyValuePair("i", _4kHeaderValue), + new KeyValuePair("j", _4kHeaderValue), + new KeyValuePair("k", _4kHeaderValue), + new KeyValuePair("l", _4kHeaderValue), + new KeyValuePair("m", _4kHeaderValue), + new KeyValuePair("n", _4kHeaderValue), + new KeyValuePair("o", _4kHeaderValue), + new KeyValuePair("p", _4kHeaderValue), }; return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize); @@ -2746,7 +2754,7 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError() [Fact] public Task HEADERS_Received_TooManyHeaders_ConnectionError() { - // > MaxRequestHeaderCount (100) + // > MaxRequestHeaderCount (100) * 2 to exceed graceful handling limit var headers = new List>(); headers.AddRange(new[] { @@ -2754,7 +2762,7 @@ public Task HEADERS_Received_TooManyHeaders_ConnectionError() new KeyValuePair(InternalHeaderNames.Path, "/"), new KeyValuePair(InternalHeaderNames.Scheme, "http"), }); - for (var i = 0; i < 100; i++) + for (var i = 0; i < 200; i++) { headers.Add(new KeyValuePair(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture))); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index b5e37940abea..bead394fd971 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; @@ -797,6 +798,77 @@ public async Task HEADERS_Received_MaxRequestLineSize_Reset() await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task HEADERS_Received_MaxRequestHeadersTotalSize_431() + { + // > 32kb + var headers = new[] + { + new KeyValuePair(InternalHeaderNames.Method, "GET"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + new KeyValuePair("a", _4kHeaderValue), + new KeyValuePair("b", _4kHeaderValue), + new KeyValuePair("c", _4kHeaderValue), + new KeyValuePair("d", _4kHeaderValue), + new KeyValuePair("e", _4kHeaderValue), + new KeyValuePair("f", _4kHeaderValue), + new KeyValuePair("g", _4kHeaderValue), + new KeyValuePair("h", _4kHeaderValue), + }; + await InitializeConnectionAsync(_noopApplication); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 40, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("431", _decodedHeaders[InternalHeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task HEADERS_Received_MaxRequestHeaderCount_431() + { + // > 100 headers + var headers = new List>() + { + new KeyValuePair(InternalHeaderNames.Method, "GET"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + }; + for (var i = 0; i < 101; i++) + { + var text = i.ToString(CultureInfo.InvariantCulture); + headers.Add(new KeyValuePair(text, text)); + } + await InitializeConnectionAsync(_noopApplication); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 40, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("431", _decodedHeaders[InternalHeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + [Fact] public async Task ContentLength_Received_SingleDataFrame_Verified() { diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs index 086a96b16356..6ab377f47815 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs @@ -1369,10 +1369,10 @@ public async Task Settings_MaxHeaderListSize_Server(string scheme) { request.Headers.Add("header" + i, oneKbString + i); } - // Kestrel closes the connection rather than sending the recommended 431 response. https://github.com/dotnet/aspnetcore/issues/17861 - await Assert.ThrowsAsync(() => client.SendAsync(request)).DefaultTimeout(); - + var response = await client.SendAsync(request).DefaultTimeout(); await host.StopAsync().DefaultTimeout(); + + Assert.Equal(HttpStatusCode.RequestHeaderFieldsTooLarge, response.StatusCode); } [Theory] From 3a63a9035c30f5521480880e2a697e4722e88cad Mon Sep 17 00:00:00 2001 From: Chris R Date: Thu, 20 Oct 2022 16:28:57 -0700 Subject: [PATCH 2/5] Send 431 when HTTP/2 headers are too large or many #33622 --- .../Core/src/Internal/Http3/Http3Stream.cs | 23 +++++- .../shared/test/Http3/Http3InMemory.cs | 6 +- .../Http3/Http3StreamTests.cs | 71 ++++++++++++++++++- 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index ea9a8ce96e3d..be5106884ada 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -267,6 +267,17 @@ public override void OnHeader(int index, bool indexOnly, ReadOnlySpan name } } + // Final: Has the message been fully received? We allow up to 2x grace while receiving the message + // to avoid faulting the stream. We check again later when we can reject the request with a 431. + protected override void CheckRequestHeadersCountLimit(bool final = false) + { + if (final && RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount + || RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount * 2) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] private void AppendHeader(ReadOnlySpan name, ReadOnlySpan value) { @@ -278,7 +289,8 @@ private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnly // https://tools.ietf.org/html/rfc7540#section-6.5.2 // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field."; _totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length; - if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize) + // Allow a 2x grace before aborting the stream. We'll check the size limit again later where we can send a 431. + if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2) { throw new Http3StreamErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected); } @@ -939,6 +951,15 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio { endConnection = !TryValidatePseudoHeaders(); + // 431 if the headers are too large + if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize); + } + + // 431 if we received too many headers + CheckRequestHeadersCountLimit(final: true); + // Suppress pseudo headers from the public headers collection. HttpRequestHeaders.ClearPseudoRequestHeaders(); diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 75dae74dcd58..7aa3d121de92 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -697,11 +697,11 @@ public async Task SendHeadersAsync(Http3HeadersEnumerator headers, bool endStrea var buffer = _headerHandler.HeaderEncodingBuffer.AsMemory(); var done = QPackHeaderWriter.BeginEncodeHeaders(headers, buffer.Span, ref headersTotalSize, out var length); - if (!done) + while (!done) { - throw new InvalidOperationException("Headers not sent."); + await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream: false); + done = QPackHeaderWriter.Encode(headers, buffer.Span, ref headersTotalSize, out length); } - await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index 884d3d9a4f4a..f6ac35291aca 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -2300,7 +2300,7 @@ await requestStream.WaitForStreamErrorAsync( } [Fact] - public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError() + public async Task HEADERS_Received_HeaderBlockOverLimit_431() { // > 32kb var headers = new[] @@ -2318,11 +2318,50 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError() new KeyValuePair("h", _4kHeaderValue), }; + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication, headers, endStream: true); + + var receivedHeaders = await requestStream.ExpectHeadersAsync(); + + await requestStream.ExpectReceiveEndOfStream(); + + Assert.Equal(3, receivedHeaders.Count); + Assert.Contains("date", receivedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("431", receivedHeaders[InternalHeaderNames.Status]); + Assert.Equal("0", receivedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public Task HEADERS_Received_HeaderBlockOverLimitx2_ConnectionError() + { + // > 32kb * 2 to exceed graceful handling limit + var headers = new[] + { + new KeyValuePair(InternalHeaderNames.Method, "GET"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + new KeyValuePair("a", _4kHeaderValue), + new KeyValuePair("b", _4kHeaderValue), + new KeyValuePair("c", _4kHeaderValue), + new KeyValuePair("d", _4kHeaderValue), + new KeyValuePair("e", _4kHeaderValue), + new KeyValuePair("f", _4kHeaderValue), + new KeyValuePair("g", _4kHeaderValue), + new KeyValuePair("h", _4kHeaderValue), + new KeyValuePair("i", _4kHeaderValue), + new KeyValuePair("j", _4kHeaderValue), + new KeyValuePair("k", _4kHeaderValue), + new KeyValuePair("l", _4kHeaderValue), + new KeyValuePair("m", _4kHeaderValue), + new KeyValuePair("n", _4kHeaderValue), + new KeyValuePair("o", _4kHeaderValue), + new KeyValuePair("p", _4kHeaderValue), + }; + return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected); } [Fact] - public Task HEADERS_Received_TooManyHeaders_ConnectionError() + public async Task HEADERS_Received_TooManyHeaders_431() { // > MaxRequestHeaderCount (100) var headers = new List>(); @@ -2337,6 +2376,34 @@ public Task HEADERS_Received_TooManyHeaders_ConnectionError() headers.Add(new KeyValuePair(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture))); } + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication, headers, endStream: true); + + var receivedHeaders = await requestStream.ExpectHeadersAsync(); + + await requestStream.ExpectReceiveEndOfStream(); + + Assert.Equal(3, receivedHeaders.Count); + Assert.Contains("date", receivedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("431", receivedHeaders[InternalHeaderNames.Status]); + Assert.Equal("0", receivedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public Task HEADERS_Received_TooManyHeadersx2_ConnectionError() + { + // > MaxRequestHeaderCount (100) * 2 to exceed graceful handling limit + var headers = new List>(); + headers.AddRange(new[] + { + new KeyValuePair(InternalHeaderNames.Method, "GET"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + }); + for (var i = 0; i < 200; i++) + { + headers.Add(new KeyValuePair(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture))); + } + return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_TooManyHeaders); } From 8ff43103defff8abd1642a1a987b8f500e0502fd Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 21 Oct 2022 11:39:09 -0700 Subject: [PATCH 3/5] Fix test encoder --- src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 7aa3d121de92..f4ecc6a6a598 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -650,7 +650,7 @@ internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, Action internal class Http3RequestHeaderHandler { - public readonly byte[] HeaderEncodingBuffer = new byte[64 * 1024]; + public readonly byte[] HeaderEncodingBuffer = new byte[96 * 1024]; public readonly QPackDecoder QpackDecoder = new QPackDecoder(8192); public readonly Dictionary DecodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -697,10 +697,9 @@ public async Task SendHeadersAsync(Http3HeadersEnumerator headers, bool endStrea var buffer = _headerHandler.HeaderEncodingBuffer.AsMemory(); var done = QPackHeaderWriter.BeginEncodeHeaders(headers, buffer.Span, ref headersTotalSize, out var length); - while (!done) + if (!done) { - await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream: false); - done = QPackHeaderWriter.Encode(headers, buffer.Span, ref headersTotalSize, out length); + throw new InvalidOperationException("The headers are too large."); } await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream); } From d7a411f4390cf83193b854929afe3f3c1827bd70 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 25 Oct 2022 12:18:53 -0700 Subject: [PATCH 4/5] PR feedback --- .../Kestrel/Core/src/Internal/Http2/Http2Connection.cs | 3 ++- .../Kestrel/Core/src/Internal/Http2/Http2Stream.cs | 2 +- .../Kestrel/Core/src/Internal/Http3/Http3Stream.cs | 9 +++++---- .../InMemory.FunctionalTests/Http2/Http2StreamTests.cs | 4 ++-- .../test/InMemory.FunctionalTests/Http2/Http2TestBase.cs | 2 ++ .../InMemory.FunctionalTests/Http3/Http3StreamTests.cs | 4 ++-- .../test/InMemory.FunctionalTests/Http3/Http3TestBase.cs | 2 ++ 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 7927bbaddad8..d194ed19c61b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1414,7 +1414,8 @@ private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnly // https://tools.ietf.org/html/rfc7540#section-6.5.2 // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field."; - _totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length; + // We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit. + _totalParsedHeaderSize += name.Length + value.Length; // Allow a 2x grace before aborting the connection. We'll check the size limit again later where we can send a 431. if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 0b5eb2c66d3b..29064cb5c27c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -202,7 +202,7 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio endConnection = !TryValidatePseudoHeaders(); // 431 if the headers are too large - if (TotalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize) + if (TotalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index be5106884ada..66a9b8db24bd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -286,11 +286,12 @@ private void AppendHeader(ReadOnlySpan name, ReadOnlySpan value) private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnlySpan name, ReadOnlySpan value) { - // https://tools.ietf.org/html/rfc7540#section-6.5.2 + // https://httpwg.org/specs/rfc9114.html#rfc.section.4.2.2 // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field."; - _totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length; + // We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit. + _totalParsedHeaderSize += name.Length + value.Length; // Allow a 2x grace before aborting the stream. We'll check the size limit again later where we can send a 431. - if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2) + if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize * 2) { throw new Http3StreamErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected); } @@ -952,7 +953,7 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio endConnection = !TryValidatePseudoHeaders(); // 431 if the headers are too large - if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize) + if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index bead394fd971..4ec15cf429f2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -816,7 +816,7 @@ public async Task HEADERS_Received_MaxRequestHeadersTotalSize_431() new KeyValuePair("g", _4kHeaderValue), new KeyValuePair("h", _4kHeaderValue), }; - await InitializeConnectionAsync(_noopApplication); + await InitializeConnectionAsync(_notImplementedApp); await StartStreamAsync(1, headers, endStream: true); @@ -850,7 +850,7 @@ public async Task HEADERS_Received_MaxRequestHeaderCount_431() var text = i.ToString(CultureInfo.InvariantCulture); headers.Add(new KeyValuePair(text, text)); } - await InitializeConnectionAsync(_noopApplication); + await InitializeConnectionAsync(_notImplementedApp); await StartStreamAsync(1, headers, endStream: true); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index ec8627bab14b..904e38992e71 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -134,6 +134,7 @@ protected static IEnumerable> ReadRateRequestHeader protected readonly TaskCompletionSource _closedStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); protected readonly RequestDelegate _noopApplication; + protected readonly RequestDelegate _notImplementedApp; protected readonly RequestDelegate _readHeadersApplication; protected readonly RequestDelegate _readTrailersApplication; protected readonly RequestDelegate _bufferingApplication; @@ -176,6 +177,7 @@ public Http2TestBase() }); _noopApplication = context => Task.CompletedTask; + _notImplementedApp = _ => throw new NotImplementedException(); _readHeadersApplication = context => { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index f6ac35291aca..79d0668d135d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -2318,7 +2318,7 @@ public async Task HEADERS_Received_HeaderBlockOverLimit_431() new KeyValuePair("h", _4kHeaderValue), }; - var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication, headers, endStream: true); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_notImplementedApp, headers, endStream: true); var receivedHeaders = await requestStream.ExpectHeadersAsync(); @@ -2376,7 +2376,7 @@ public async Task HEADERS_Received_TooManyHeaders_431() headers.Add(new KeyValuePair(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture))); } - var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication, headers, endStream: true); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_notImplementedApp, headers, endStream: true); var receivedHeaders = await requestStream.ExpectHeadersAsync(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index dc28eafb46d9..b5d0f612afd5 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -49,6 +49,7 @@ public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDis internal readonly Mock _mockTimeoutHandler = new Mock(); protected readonly RequestDelegate _noopApplication; + protected readonly RequestDelegate _notImplementedApp; protected readonly RequestDelegate _echoApplication; protected readonly RequestDelegate _readRateApplication; protected readonly RequestDelegate _echoMethod; @@ -79,6 +80,7 @@ protected static IEnumerable> ReadRateRequestHeader public Http3TestBase() { _noopApplication = context => Task.CompletedTask; + _notImplementedApp = _ => throw new NotImplementedException(); _echoApplication = async context => { From 3be5385dd5ac751f422a66cac19b33eb696ab6c7 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 25 Oct 2022 14:42:16 -0700 Subject: [PATCH 5/5] Rework count enforcement --- .../Core/src/Internal/Http/HttpProtocol.cs | 10 ++++------ .../Core/src/Internal/Http2/Http2Stream.cs | 18 ++++++------------ .../Core/src/Internal/Http3/Http3Stream.cs | 18 ++++++------------ .../Core/test/Http1/Http1ConnectionTests.cs | 1 + 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index a2befc02747b..1ae6112104d9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -63,6 +63,8 @@ internal abstract partial class HttpProtocol : IHttpResponseControl private string? _requestId; private int _requestHeadersParsed; + // See MaxRequestHeaderCount, enforced during parsing and may be more relaxed to avoid connection faults. + protected int _eagerRequestHeadersParsedLimit; private long _responseBytesWritten; @@ -417,6 +419,7 @@ public void Reset() Output?.Reset(); _requestHeadersParsed = 0; + _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount; _responseBytesWritten = 0; @@ -547,12 +550,7 @@ public void OnTrailer(ReadOnlySpan name, ReadOnlySpan value) private void IncrementRequestHeadersCount() { _requestHeadersParsed++; - CheckRequestHeadersCountLimit(); - } - - protected virtual void CheckRequestHeadersCountLimit(bool final = false) - { - if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) + if (_requestHeadersParsed > _eagerRequestHeadersParsedLimit) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 29064cb5c27c..9e9ff968a3a7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -44,6 +44,8 @@ public void Initialize(Http2StreamContext context) RequestBodyStarted = false; DrainExpirationTicks = 0; TotalParsedHeaderSize = 0; + // Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection. + _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2; _context = context; @@ -208,7 +210,10 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio } // 431 if we received too many headers - CheckRequestHeadersCountLimit(final: true); + if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + } // Suppress pseudo headers from the public headers collection. HttpRequestHeaders.ClearPseudoRequestHeaders(); @@ -707,17 +712,6 @@ public override void OnHeader(int index, bool indexOnly, ReadOnlySpan name } } - // Final: Has the message been fully received? We allow up to 2x grace while receiving the message - // to avoid faulting the entire connection. We check again later when we can reject the request with a 431. - protected override void CheckRequestHeadersCountLimit(bool final = false) - { - if (final && RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount - || RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount * 2) - { - KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); - } - } - [MethodImpl(MethodImplOptions.NoInlining)] private void AppendHeader(ReadOnlySpan name, ReadOnlySpan value) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 66a9b8db24bd..aaf4cd8e645f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -96,6 +96,8 @@ public void Initialize(Http3StreamContext context) _requestHeaderParsingState = default; _parsedPseudoHeaderFields = default; _totalParsedHeaderSize = 0; + // Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection. + _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2; _isMethodConnect = false; _completionState = default; StreamTimeoutTicks = 0; @@ -267,17 +269,6 @@ public override void OnHeader(int index, bool indexOnly, ReadOnlySpan name } } - // Final: Has the message been fully received? We allow up to 2x grace while receiving the message - // to avoid faulting the stream. We check again later when we can reject the request with a 431. - protected override void CheckRequestHeadersCountLimit(bool final = false) - { - if (final && RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount - || RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount * 2) - { - KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); - } - } - [MethodImpl(MethodImplOptions.NoInlining)] private void AppendHeader(ReadOnlySpan name, ReadOnlySpan value) { @@ -959,7 +950,10 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio } // 431 if we received too many headers - CheckRequestHeadersCountLimit(final: true); + if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + } // Suppress pseudo headers from the public headers collection. HttpRequestHeaders.ClearPseudoRequestHeaders(); diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs index 8091afec1044..cd1cc5b208bd 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs @@ -89,6 +89,7 @@ public async Task TakeMessageHeadersThrowsWhenHeadersExceedCountLimit() { const string headerLines = "Header-1: value1\r\nHeader-2: value2\r\n"; _serviceContext.ServerOptions.Limits.MaxRequestHeaderCount = 1; + _http1Connection.Initialize(_http1ConnectionContext); await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;