Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Send 431 when HTTP/2 headers are too large or many #33622
  • Loading branch information
Tratcher authored and github-actions committed Oct 27, 2022
commit 2e24a307b33575ba1eabeaff9071cf18d5ce3788
23 changes: 22 additions & 1 deletion src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,17 @@ public override void OnHeader(int index, bool indexOnly, ReadOnlySpan<byte> 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<byte> name, ReadOnlySpan<byte> value)
{
Expand All @@ -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);
}
Expand Down Expand Up @@ -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();

Expand Down
6 changes: 3 additions & 3 deletions src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -2318,11 +2318,50 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
new KeyValuePair<string, string>("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<string, string>(InternalHeaderNames.Method, "GET"),
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
new KeyValuePair<string, string>("a", _4kHeaderValue),
new KeyValuePair<string, string>("b", _4kHeaderValue),
new KeyValuePair<string, string>("c", _4kHeaderValue),
new KeyValuePair<string, string>("d", _4kHeaderValue),
new KeyValuePair<string, string>("e", _4kHeaderValue),
new KeyValuePair<string, string>("f", _4kHeaderValue),
new KeyValuePair<string, string>("g", _4kHeaderValue),
new KeyValuePair<string, string>("h", _4kHeaderValue),
new KeyValuePair<string, string>("i", _4kHeaderValue),
new KeyValuePair<string, string>("j", _4kHeaderValue),
new KeyValuePair<string, string>("k", _4kHeaderValue),
new KeyValuePair<string, string>("l", _4kHeaderValue),
new KeyValuePair<string, string>("m", _4kHeaderValue),
new KeyValuePair<string, string>("n", _4kHeaderValue),
new KeyValuePair<string, string>("o", _4kHeaderValue),
new KeyValuePair<string, string>("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<KeyValuePair<string, string>>();
Expand All @@ -2337,6 +2376,34 @@ public Task HEADERS_Received_TooManyHeaders_ConnectionError()
headers.Add(new KeyValuePair<string, string>(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<KeyValuePair<string, string>>();
headers.AddRange(new[]
{
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
});
for (var i = 0; i < 200; i++)
{
headers.Add(new KeyValuePair<string, string>(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture)));
}

return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_TooManyHeaders);
}

Expand Down