Skip to content

Commit f490fe4

Browse files
[release/7.0] Send 431 when HTTP/2&3 headers are too large or many (#44767)
* Send 431 when HTTP/2 headers are too large or many #17861 * Send 431 when HTTP/2 headers are too large or many #33622 * Fix test encoder * PR feedback * Rework count enforcement Co-authored-by: Chris R <[email protected]>
1 parent 8350b94 commit f490fe4

File tree

12 files changed

+209
-17
lines changed

12 files changed

+209
-17
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ internal abstract partial class HttpProtocol : IHttpResponseControl
6363

6464
private string? _requestId;
6565
private int _requestHeadersParsed;
66+
// See MaxRequestHeaderCount, enforced during parsing and may be more relaxed to avoid connection faults.
67+
protected int _eagerRequestHeadersParsedLimit;
6668

6769
private long _responseBytesWritten;
6870

@@ -107,6 +109,7 @@ public void Initialize(HttpConnectionContext context)
107109
public long? MaxRequestBodySize { get; set; }
108110
public MinDataRate? MinRequestBodyDataRate { get; set; }
109111
public bool AllowSynchronousIO { get; set; }
112+
protected int RequestHeadersParsed => _requestHeadersParsed;
110113

111114
/// <summary>
112115
/// The request id. <seealso cref="HttpContext.TraceIdentifier"/>
@@ -416,6 +419,7 @@ public void Reset()
416419
Output?.Reset();
417420

418421
_requestHeadersParsed = 0;
422+
_eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount;
419423

420424
_responseBytesWritten = 0;
421425

@@ -546,7 +550,7 @@ public void OnTrailer(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
546550
private void IncrementRequestHeadersCount()
547551
{
548552
_requestHeadersParsed++;
549-
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
553+
if (_requestHeadersParsed > _eagerRequestHeadersParsedLimit)
550554
{
551555
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
552556
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,8 @@ private void StartStream()
11281128

11291129
try
11301130
{
1131+
_currentHeadersStream.TotalParsedHeaderSize = _totalParsedHeaderSize;
1132+
11311133
// This must be initialized before we offload the request or else we may start processing request body frames without it.
11321134
_currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength;
11331135

@@ -1410,8 +1412,10 @@ private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnly
14101412

14111413
// https://tools.ietf.org/html/rfc7540#section-6.5.2
14121414
// "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.";
1413-
_totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
1414-
if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
1415+
// We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit.
1416+
_totalParsedHeaderSize += name.Length + value.Length;
1417+
// Allow a 2x grace before aborting the connection. We'll check the size limit again later where we can send a 431.
1418+
if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2)
14151419
{
14161420
throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR);
14171421
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ internal abstract partial class Http2Stream : HttpProtocol, IThreadPoolWorkItem,
2525

2626
private bool _decrementCalled;
2727

28+
public int TotalParsedHeaderSize { get; set; }
29+
2830
public Pipe RequestBodyPipe { get; private set; } = default!;
2931

3032
internal long DrainExpirationTicks { get; set; }
@@ -41,6 +43,9 @@ public void Initialize(Http2StreamContext context)
4143
InputRemaining = null;
4244
RequestBodyStarted = false;
4345
DrainExpirationTicks = 0;
46+
TotalParsedHeaderSize = 0;
47+
// Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection.
48+
_eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2;
4449

4550
_context = context;
4651

@@ -198,6 +203,18 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
198203
// do the reading from a pipeline, nor do we use endConnection to report connection-level errors.
199204
endConnection = !TryValidatePseudoHeaders();
200205

206+
// 431 if the headers are too large
207+
if (TotalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize)
208+
{
209+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
210+
}
211+
212+
// 431 if we received too many headers
213+
if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
214+
{
215+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
216+
}
217+
201218
// Suppress pseudo headers from the public headers collection.
202219
HttpRequestHeaders.ClearPseudoRequestHeaders();
203220

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ public void Initialize(Http3StreamContext context)
9696
_requestHeaderParsingState = default;
9797
_parsedPseudoHeaderFields = default;
9898
_totalParsedHeaderSize = 0;
99+
// Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection.
100+
_eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2;
99101
_isMethodConnect = false;
100102
_completionState = default;
101103
StreamTimeoutTicks = 0;
@@ -275,10 +277,12 @@ private void AppendHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
275277

276278
private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
277279
{
278-
// https://tools.ietf.org/html/rfc7540#section-6.5.2
280+
// https://httpwg.org/specs/rfc9114.html#rfc.section.4.2.2
279281
// "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.";
280-
_totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
281-
if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
282+
// We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit.
283+
_totalParsedHeaderSize += name.Length + value.Length;
284+
// Allow a 2x grace before aborting the stream. We'll check the size limit again later where we can send a 431.
285+
if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize * 2)
282286
{
283287
throw new Http3StreamErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
284288
}
@@ -939,6 +943,18 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
939943
{
940944
endConnection = !TryValidatePseudoHeaders();
941945

946+
// 431 if the headers are too large
947+
if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize)
948+
{
949+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
950+
}
951+
952+
// 431 if we received too many headers
953+
if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
954+
{
955+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
956+
}
957+
942958
// Suppress pseudo headers from the public headers collection.
943959
HttpRequestHeaders.ClearPseudoRequestHeaders();
944960

src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public async Task TakeMessageHeadersThrowsWhenHeadersExceedCountLimit()
8888
{
8989
const string headerLines = "Header-1: value1\r\nHeader-2: value2\r\n";
9090
_serviceContext.ServerOptions.Limits.MaxRequestHeaderCount = 1;
91+
_http1Connection.Initialize(_http1ConnectionContext);
9192

9293
await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n"));
9394
var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;

src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, Action
650650

651651
internal class Http3RequestHeaderHandler
652652
{
653-
public readonly byte[] HeaderEncodingBuffer = new byte[64 * 1024];
653+
public readonly byte[] HeaderEncodingBuffer = new byte[96 * 1024];
654654
public readonly QPackDecoder QpackDecoder = new QPackDecoder(8192);
655655
public readonly Dictionary<string, string> DecodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
656656
}
@@ -699,9 +699,8 @@ public async Task SendHeadersAsync(Http3HeadersEnumerator headers, bool endStrea
699699
var done = QPackHeaderWriter.BeginEncodeHeaders(headers, buffer.Span, ref headersTotalSize, out var length);
700700
if (!done)
701701
{
702-
throw new InvalidOperationException("Headers not sent.");
702+
throw new InvalidOperationException("The headers are too large.");
703703
}
704-
705704
await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream);
706705
}
707706

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2724,7 +2724,7 @@ await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(
27242724
[Fact]
27252725
public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
27262726
{
2727-
// > 32kb
2727+
// > 32kb * 2 to exceed graceful handling limit
27282728
var headers = new[]
27292729
{
27302730
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
@@ -2738,6 +2738,14 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
27382738
new KeyValuePair<string, string>("f", _4kHeaderValue),
27392739
new KeyValuePair<string, string>("g", _4kHeaderValue),
27402740
new KeyValuePair<string, string>("h", _4kHeaderValue),
2741+
new KeyValuePair<string, string>("i", _4kHeaderValue),
2742+
new KeyValuePair<string, string>("j", _4kHeaderValue),
2743+
new KeyValuePair<string, string>("k", _4kHeaderValue),
2744+
new KeyValuePair<string, string>("l", _4kHeaderValue),
2745+
new KeyValuePair<string, string>("m", _4kHeaderValue),
2746+
new KeyValuePair<string, string>("n", _4kHeaderValue),
2747+
new KeyValuePair<string, string>("o", _4kHeaderValue),
2748+
new KeyValuePair<string, string>("p", _4kHeaderValue),
27412749
};
27422750

27432751
return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize);
@@ -2746,15 +2754,15 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
27462754
[Fact]
27472755
public Task HEADERS_Received_TooManyHeaders_ConnectionError()
27482756
{
2749-
// > MaxRequestHeaderCount (100)
2757+
// > MaxRequestHeaderCount (100) * 2 to exceed graceful handling limit
27502758
var headers = new List<KeyValuePair<string, string>>();
27512759
headers.AddRange(new[]
27522760
{
27532761
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
27542762
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
27552763
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
27562764
});
2757-
for (var i = 0; i < 100; i++)
2765+
for (var i = 0; i < 200; i++)
27582766
{
27592767
headers.Add(new KeyValuePair<string, string>(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture)));
27602768
}

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Buffers;
66
using System.Collections.Generic;
7+
using System.Globalization;
78
using System.IO;
89
using System.Linq;
910
using System.Net.Http;
@@ -797,6 +798,77 @@ public async Task HEADERS_Received_MaxRequestLineSize_Reset()
797798
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
798799
}
799800

801+
[Fact]
802+
public async Task HEADERS_Received_MaxRequestHeadersTotalSize_431()
803+
{
804+
// > 32kb
805+
var headers = new[]
806+
{
807+
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
808+
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
809+
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
810+
new KeyValuePair<string, string>("a", _4kHeaderValue),
811+
new KeyValuePair<string, string>("b", _4kHeaderValue),
812+
new KeyValuePair<string, string>("c", _4kHeaderValue),
813+
new KeyValuePair<string, string>("d", _4kHeaderValue),
814+
new KeyValuePair<string, string>("e", _4kHeaderValue),
815+
new KeyValuePair<string, string>("f", _4kHeaderValue),
816+
new KeyValuePair<string, string>("g", _4kHeaderValue),
817+
new KeyValuePair<string, string>("h", _4kHeaderValue),
818+
};
819+
await InitializeConnectionAsync(_notImplementedApp);
820+
821+
await StartStreamAsync(1, headers, endStream: true);
822+
823+
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
824+
withLength: 40,
825+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
826+
withStreamId: 1);
827+
828+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
829+
830+
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
831+
832+
Assert.Equal(3, _decodedHeaders.Count);
833+
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
834+
Assert.Equal("431", _decodedHeaders[InternalHeaderNames.Status]);
835+
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
836+
}
837+
838+
[Fact]
839+
public async Task HEADERS_Received_MaxRequestHeaderCount_431()
840+
{
841+
// > 100 headers
842+
var headers = new List<KeyValuePair<string, string>>()
843+
{
844+
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
845+
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
846+
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
847+
};
848+
for (var i = 0; i < 101; i++)
849+
{
850+
var text = i.ToString(CultureInfo.InvariantCulture);
851+
headers.Add(new KeyValuePair<string, string>(text, text));
852+
}
853+
await InitializeConnectionAsync(_notImplementedApp);
854+
855+
await StartStreamAsync(1, headers, endStream: true);
856+
857+
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
858+
withLength: 40,
859+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
860+
withStreamId: 1);
861+
862+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
863+
864+
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
865+
866+
Assert.Equal(3, _decodedHeaders.Count);
867+
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
868+
Assert.Equal("431", _decodedHeaders[InternalHeaderNames.Status]);
869+
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
870+
}
871+
800872
[Fact]
801873
public async Task ContentLength_Received_SingleDataFrame_Verified()
802874
{

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ protected static IEnumerable<KeyValuePair<string, string>> ReadRateRequestHeader
134134
protected readonly TaskCompletionSource _closedStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
135135

136136
protected readonly RequestDelegate _noopApplication;
137+
protected readonly RequestDelegate _notImplementedApp;
137138
protected readonly RequestDelegate _readHeadersApplication;
138139
protected readonly RequestDelegate _readTrailersApplication;
139140
protected readonly RequestDelegate _bufferingApplication;
@@ -176,6 +177,7 @@ public Http2TestBase()
176177
});
177178

178179
_noopApplication = context => Task.CompletedTask;
180+
_notImplementedApp = _ => throw new NotImplementedException();
179181

180182
_readHeadersApplication = context =>
181183
{

0 commit comments

Comments
 (0)