From e536cc6102a21a53892c14f5728d6b32d06cd00a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:17:26 +0000 Subject: [PATCH 1/2] Initial plan From 4fc70e635d332dd6020f3769435854daf1752951 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:49:49 +0000 Subject: [PATCH 2/2] Apply HPack/QPack Huffman decoded header length check fix from workflow run 23059322485 Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../aspnetcore/Http2/Hpack/HPackDecoder.cs | 8 +++- .../aspnetcore/Http3/QPack/QPackDecoder.cs | 8 +++- .../Net/aspnetcore/Http2/HPackDecoderTest.cs | 39 +++++++++++++++++ .../Net/aspnetcore/Http3/QPackDecoderTest.cs | 43 +++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackDecoder.cs b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackDecoder.cs index 6b4fcb3a77cad6..4b08ef37010e74 100644 --- a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackDecoder.cs +++ b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackDecoder.cs @@ -596,10 +596,16 @@ int Decode(ref byte[] dst) { if (_huffman) { - return Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), ref dst); + int decodedLength = Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), ref dst); + if (decodedLength > _maxHeadersLength) + { + throw new HPackDecodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxHeadersLength)); + } + return decodedLength; } else { + Debug.Assert(_stringLength <= _maxHeadersLength, "String length should have been checked prior to decode."); EnsureStringCapacity(ref dst); Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); return _stringLength; diff --git a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/QPackDecoder.cs b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/QPackDecoder.cs index ea575da49391f3..155bb5fc8ca6bc 100644 --- a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/QPackDecoder.cs +++ b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/QPackDecoder.cs @@ -634,10 +634,16 @@ int Decode(ref byte[]? dst) if (_huffman) { - return Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), ref dst); + int decodedLength = Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), ref dst); + if (decodedLength > _maxHeadersLength) + { + throw new QPackDecodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxHeadersLength)); + } + return decodedLength; } else { + Debug.Assert(_stringLength <= _maxHeadersLength, "String length should have been checked prior to decode."); Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); return _stringLength; } diff --git a/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http2/HPackDecoderTest.cs b/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http2/HPackDecoderTest.cs index 2dde54407bcdad..9081b5becc5dfa 100644 --- a/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http2/HPackDecoderTest.cs +++ b/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http2/HPackDecoderTest.cs @@ -678,6 +678,45 @@ public void DecodesStringLength_GreaterThanLimit_Error() Assert.Empty(_handler.DecodedHeaders); } + [Fact] + public void HuffmanDecodedHeaderName_ExceedsLimitAfterDecoding_Throws() + { + // '0' (ASCII 48) has a 5-bit Huffman code (00000). + // 16 '0' characters = 80 Huffman bits = 10 encoded bytes, but decodes to 16 bytes. + // Encoded length (10) passes the pre-decode check (<= 10), but decoded length (16) exceeds the limit. + HPackDecoder decoder = new HPackDecoder(DynamicTableInitialMaxSize, maxHeadersLength: 10); + + byte[] encoded = [ + 0x00, // Literal header field without indexing, new name + 0x8a, // Huffman flag set (0x80) + string length 10 (7-bit prefix) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10 bytes Huffman -> 16 '0' chars + 0x01, // String length = 1 (no Huffman) + 0x61 // Value = "a" + ]; + + HPackDecodingException exception = Assert.Throws(() => decoder.Decode(encoded, endHeaders: true, handler: _handler)); + Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, 10), exception.Message); + Assert.Empty(_handler.DecodedHeaders); + } + + [Fact] + public void HuffmanDecodedHeaderValue_ExceedsLimitAfterDecoding_Throws() + { + HPackDecoder decoder = new HPackDecoder(DynamicTableInitialMaxSize, maxHeadersLength: 10); + + byte[] encoded = [ + 0x00, // Literal header field without indexing, new name + 0x01, // String length = 1 (no Huffman) + 0x61, // Name = "a" + 0x8a, // Huffman flag set (0x80) + string length 10 (7-bit prefix) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // 10 bytes Huffman -> 16 '0' chars + ]; + + HPackDecodingException exception = Assert.Throws(() => decoder.Decode(encoded, endHeaders: true, handler: _handler)); + Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, 10), exception.Message); + Assert.Empty(_handler.DecodedHeaders); + } + [Fact] public void DecodesStringLength_LimitConfigurable() { diff --git a/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http3/QPackDecoderTest.cs b/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http3/QPackDecoderTest.cs index 643bfdd868c06f..e78b93517aa0f8 100644 --- a/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http3/QPackDecoderTest.cs +++ b/src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http3/QPackDecoderTest.cs @@ -273,6 +273,49 @@ public void LiteralFieldWithoutNameReference_ValueBrokenIntoSeparateBuffers() Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderFieldString]); } + [Fact] + public void HuffmanDecodedHeaderName_ExceedsLimitAfterDecoding_Throws() + { + // '0' (ASCII 48) has a 5-bit Huffman code (00000). + // 16 '0' characters = 80 Huffman bits = 10 encoded bytes, but decodes to 16 bytes. + // Encoded length (10) passes the pre-decode check (<= 10), but decoded length (16) exceeds the limit. + using QPackDecoder decoder = new QPackDecoder(maxHeadersLength: 10); + TestHttpHeadersHandler handler = new TestHttpHeadersHandler(); + + byte[] encoded = new byte[] + { + 0x00, 0x00, // Required insert count (0) and base (0) + 0x2f, 0x03, // Literal field with literal name: N=0, H=1, name length = 10 (3-bit prefix: 7 + 3) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10 bytes Huffman -> 16 '0' chars + 0x01, // Value length = 1 (no Huffman) + 0x61 // Value = "a" + }; + + QPackDecodingException exception = Assert.Throws(() => decoder.Decode(encoded, endHeaders: true, handler: handler)); + Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, 10), exception.Message); + Assert.Empty(handler.DecodedHeaders); + } + + [Fact] + public void HuffmanDecodedHeaderValue_ExceedsLimitAfterDecoding_Throws() + { + using QPackDecoder decoder = new QPackDecoder(maxHeadersLength: 10); + TestHttpHeadersHandler handler = new TestHttpHeadersHandler(); + + byte[] encoded = new byte[] + { + 0x00, 0x00, // Required insert count (0) and base (0) + 0x21, // Literal field with literal name: N=0, H=0, name length = 1 (3-bit prefix) + 0x61, // Name = "a" + 0x8a, // Huffman flag set (0x80) + value length 10 (7-bit prefix) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // 10 bytes Huffman -> 16 '0' chars + }; + + QPackDecodingException exception = Assert.Throws(() => decoder.Decode(encoded, endHeaders: true, handler: handler)); + Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, 10), exception.Message); + Assert.Empty(handler.DecodedHeaders); + } + public static readonly TheoryData _incompleteHeaderBlockData = new TheoryData { // Incomplete header