Skip to content

Commit aa5924b

Browse files
authored
Send 431 when HTTP/2&3 headers are too large or many #33622 (#44771)
1 parent 835fb32 commit aa5924b

File tree

12 files changed

+212
-17
lines changed

12 files changed

+212
-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
@@ -68,6 +68,8 @@ internal abstract partial class HttpProtocol : IHttpResponseControl
6868

6969
private string? _requestId;
7070
private int _requestHeadersParsed;
71+
// See MaxRequestHeaderCount, enforced during parsing and may be more relaxed to avoid connection faults.
72+
protected int _eagerRequestHeadersParsedLimit;
7173

7274
private long _responseBytesWritten;
7375

@@ -112,6 +114,7 @@ public void Initialize(HttpConnectionContext context)
112114
public long? MaxRequestBodySize { get; set; }
113115
public MinDataRate? MinRequestBodyDataRate { get; set; }
114116
public bool AllowSynchronousIO { get; set; }
117+
protected int RequestHeadersParsed => _requestHeadersParsed;
115118

116119
/// <summary>
117120
/// The request id. <seealso cref="HttpContext.TraceIdentifier"/>
@@ -413,6 +416,7 @@ public void Reset()
413416
Output?.Reset();
414417

415418
_requestHeadersParsed = 0;
419+
_eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount;
416420

417421
_responseBytesWritten = 0;
418422

@@ -544,7 +548,7 @@ public void OnTrailer(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
544548
private void IncrementRequestHeadersCount()
545549
{
546550
_requestHeadersParsed++;
547-
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
551+
if (_requestHeadersParsed > _eagerRequestHeadersParsedLimit)
548552
{
549553
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
550554
}

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

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

10201020
try
10211021
{
1022+
_currentHeadersStream.TotalParsedHeaderSize = _totalParsedHeaderSize;
1023+
10221024
// This must be initialized before we offload the request or else we may start processing request body frames without it.
10231025
_currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength;
10241026

@@ -1279,8 +1281,10 @@ private void OnHeaderCore(int? index, bool indexedValue, ReadOnlySpan<byte> name
12791281

12801282
// https://tools.ietf.org/html/rfc7540#section-6.5.2
12811283
// "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.";
1282-
_totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
1283-
if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
1284+
// We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit.
1285+
_totalParsedHeaderSize += name.Length + value.Length;
1286+
// Allow a 2x grace before aborting the connection. We'll check the size limit again later where we can send a 431.
1287+
if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2)
12841288
{
12851289
throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR);
12861290
}

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

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

3232
private bool _decrementCalled;
3333

34+
public int TotalParsedHeaderSize { get; set; }
35+
3436
public Pipe RequestBodyPipe { get; private set; } = default!;
3537

3638
internal long DrainExpirationTicks { get; set; }
@@ -47,6 +49,9 @@ public void Initialize(Http2StreamContext context)
4749
InputRemaining = null;
4850
RequestBodyStarted = false;
4951
DrainExpirationTicks = 0;
52+
TotalParsedHeaderSize = 0;
53+
// Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection.
54+
_eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2;
5055

5156
_context = context;
5257

@@ -208,6 +213,19 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
208213
// We don't need any of the parameters because we don't implement BeginRead to actually
209214
// do the reading from a pipeline, nor do we use endConnection to report connection-level errors.
210215
endConnection = !TryValidatePseudoHeaders();
216+
217+
// 431 if the headers are too large
218+
if (TotalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize)
219+
{
220+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
221+
}
222+
223+
// 431 if we received too many headers
224+
if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
225+
{
226+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
227+
}
228+
211229
return true;
212230
}
213231

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public void Initialize(Http3StreamContext context)
9292
_requestHeaderParsingState = default;
9393
_parsedPseudoHeaderFields = default;
9494
_totalParsedHeaderSize = 0;
95+
// Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection.
96+
_eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2;
9597
_isMethodConnect = false;
9698
_completionState = default;
9799
StreamTimeoutTicks = 0;
@@ -205,10 +207,12 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
205207

206208
public override void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value, bool checkForNewlineChars)
207209
{
208-
// https://tools.ietf.org/html/rfc7540#section-6.5.2
210+
// https://httpwg.org/specs/rfc9114.html#rfc.section.4.2.2
209211
// "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.";
210-
_totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
211-
if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
212+
// We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit.
213+
_totalParsedHeaderSize += name.Length + value.Length;
214+
// Allow a 2x grace before aborting the stream. We'll check the size limit again later where we can send a 431.
215+
if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize * 2)
212216
{
213217
throw new Http3StreamErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
214218
}
@@ -754,6 +758,19 @@ protected override MessageBody CreateMessageBody()
754758
protected override bool TryParseRequest(ReadResult result, out bool endConnection)
755759
{
756760
endConnection = !TryValidatePseudoHeaders();
761+
762+
// 431 if the headers are too large
763+
if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize)
764+
{
765+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
766+
}
767+
768+
// 431 if we received too many headers
769+
if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
770+
{
771+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
772+
}
773+
757774
return true;
758775
}
759776

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

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

141142
await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n"));
142143
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
@@ -590,7 +590,7 @@ internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, Action
590590

591591
internal class Http3RequestHeaderHandler
592592
{
593-
public readonly byte[] HeaderEncodingBuffer = new byte[64 * 1024];
593+
public readonly byte[] HeaderEncodingBuffer = new byte[96 * 1024];
594594
public readonly QPackDecoder QpackDecoder = new QPackDecoder(8192);
595595
public readonly Dictionary<string, string> DecodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
596596
}
@@ -639,9 +639,8 @@ public async Task SendHeadersAsync(Http3HeadersEnumerator headers, bool endStrea
639639
var done = QPackHeaderWriter.BeginEncode(headers, buffer.Span, ref headersTotalSize, out var length);
640640
if (!done)
641641
{
642-
throw new InvalidOperationException("Headers not sent.");
642+
throw new InvalidOperationException("The headers are too large.");
643643
}
644-
645644
await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream);
646645
}
647646

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2712,7 +2712,7 @@ await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(
27122712
[Fact]
27132713
public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
27142714
{
2715-
// > 32kb
2715+
// > 32kb * 2 to exceed graceful handling limit
27162716
var headers = new[]
27172717
{
27182718
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
@@ -2726,6 +2726,14 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
27262726
new KeyValuePair<string, string>("f", _4kHeaderValue),
27272727
new KeyValuePair<string, string>("g", _4kHeaderValue),
27282728
new KeyValuePair<string, string>("h", _4kHeaderValue),
2729+
new KeyValuePair<string, string>("i", _4kHeaderValue),
2730+
new KeyValuePair<string, string>("j", _4kHeaderValue),
2731+
new KeyValuePair<string, string>("k", _4kHeaderValue),
2732+
new KeyValuePair<string, string>("l", _4kHeaderValue),
2733+
new KeyValuePair<string, string>("m", _4kHeaderValue),
2734+
new KeyValuePair<string, string>("n", _4kHeaderValue),
2735+
new KeyValuePair<string, string>("o", _4kHeaderValue),
2736+
new KeyValuePair<string, string>("p", _4kHeaderValue),
27292737
};
27302738

27312739
return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize);
@@ -2734,15 +2742,15 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
27342742
[Fact]
27352743
public Task HEADERS_Received_TooManyHeaders_ConnectionError()
27362744
{
2737-
// > MaxRequestHeaderCount (100)
2745+
// > MaxRequestHeaderCount (100) * 2 to exceed graceful handling limit
27382746
var headers = new List<KeyValuePair<string, string>>();
27392747
headers.AddRange(new[]
27402748
{
27412749
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
27422750
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
27432751
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
27442752
});
2745-
for (var i = 0; i < 100; i++)
2753+
for (var i = 0; i < 200; i++)
27462754
{
27472755
headers.Add(new KeyValuePair<string, string>(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture)));
27482756
}

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;
@@ -740,6 +741,77 @@ public async Task HEADERS_Received_MaxRequestLineSize_Reset()
740741
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
741742
}
742743

744+
[Fact]
745+
public async Task HEADERS_Received_MaxRequestHeadersTotalSize_431()
746+
{
747+
// > 32kb
748+
var headers = new[]
749+
{
750+
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
751+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
752+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
753+
new KeyValuePair<string, string>("a", _4kHeaderValue),
754+
new KeyValuePair<string, string>("b", _4kHeaderValue),
755+
new KeyValuePair<string, string>("c", _4kHeaderValue),
756+
new KeyValuePair<string, string>("d", _4kHeaderValue),
757+
new KeyValuePair<string, string>("e", _4kHeaderValue),
758+
new KeyValuePair<string, string>("f", _4kHeaderValue),
759+
new KeyValuePair<string, string>("g", _4kHeaderValue),
760+
new KeyValuePair<string, string>("h", _4kHeaderValue),
761+
};
762+
await InitializeConnectionAsync(_notImplementedApp);
763+
764+
await StartStreamAsync(1, headers, endStream: true);
765+
766+
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
767+
withLength: 40,
768+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
769+
withStreamId: 1);
770+
771+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
772+
773+
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
774+
775+
Assert.Equal(3, _decodedHeaders.Count);
776+
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
777+
Assert.Equal("431", _decodedHeaders[HeaderNames.Status]);
778+
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
779+
}
780+
781+
[Fact]
782+
public async Task HEADERS_Received_MaxRequestHeaderCount_431()
783+
{
784+
// > 100 headers
785+
var headers = new List<KeyValuePair<string, string>>()
786+
{
787+
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
788+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
789+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
790+
};
791+
for (var i = 0; i < 101; i++)
792+
{
793+
var text = i.ToString(CultureInfo.InvariantCulture);
794+
headers.Add(new KeyValuePair<string, string>(text, text));
795+
}
796+
await InitializeConnectionAsync(_notImplementedApp);
797+
798+
await StartStreamAsync(1, headers, endStream: true);
799+
800+
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
801+
withLength: 40,
802+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
803+
withStreamId: 1);
804+
805+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
806+
807+
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
808+
809+
Assert.Equal(3, _decodedHeaders.Count);
810+
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
811+
Assert.Equal("431", _decodedHeaders[HeaderNames.Status]);
812+
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
813+
}
814+
743815
[Fact]
744816
public async Task ContentLength_Received_SingleDataFrame_Verified()
745817
{

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

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

145145
protected readonly RequestDelegate _noopApplication;
146+
protected readonly RequestDelegate _notImplementedApp;
146147
protected readonly RequestDelegate _readHeadersApplication;
147148
protected readonly RequestDelegate _readTrailersApplication;
148149
protected readonly RequestDelegate _bufferingApplication;
@@ -193,6 +194,7 @@ public Http2TestBase()
193194
});
194195

195196
_noopApplication = context => Task.CompletedTask;
197+
_notImplementedApp = _ => throw new NotImplementedException();
196198

197199
_readHeadersApplication = context =>
198200
{

0 commit comments

Comments
 (0)