/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Shared/runtime/Http2/Hpack/HPackDecoder.cs
687 строк
27 KB
Korolev Dmitry
fix: Q/HPackDecoder limit validation after decoding (#65771)
13 мар 2026, 17:09
Не верифицирован
13 мар 2026, 17:09
c892018
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System.Buffers; using System.Diagnostics; using System.Numerics; #if KESTREL using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; #endif namespace System.Net.Http.HPack { internal sealed class HPackDecoder { private enum State : byte { Ready, HeaderFieldIndex, HeaderNameIndex, HeaderNameLength, HeaderNameLengthContinue, HeaderName, HeaderValueLength, HeaderValueLengthContinue, HeaderValue, DynamicTableSizeUpdate } // https://datatracker.ietf.org/doc/html/rfc9113#name-defined-settings // Initial value for SETTINGS_HEADER_TABLE_SIZE is 4,096 octets. public const int DefaultHeaderTableSize = 4096; // This is the initial size. Buffers will be dynamically resized as needed. public const int DefaultStringOctetsSize = 32; public const int DefaultMaxHeadersLength = 64 * 1024; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.1 // 0 1 2 3 4 5 6 7 // +---+---+---+---+---+---+---+---+ // | 1 | Index (7+) | // +---+---------------------------+ private const byte IndexedHeaderFieldMask = 0x80; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1 // 0 1 2 3 4 5 6 7 // +---+---+---+---+---+---+---+---+ // | 0 | 1 | Index (6+) | // +---+---+-----------------------+ private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2 // 0 1 2 3 4 5 6 7 // +---+---+---+---+---+---+---+---+ // | 0 | 0 | 0 | 0 | Index (4+) | // +---+---+-----------------------+ private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3 // 0 1 2 3 4 5 6 7 // +---+---+---+---+---+---+---+---+ // | 0 | 0 | 0 | 1 | Index (4+) | // +---+---+-----------------------+ private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.3 // 0 1 2 3 4 5 6 7 // +---+---+---+---+---+---+---+---+ // | 0 | 0 | 1 | Max size (5+) | // +---+---------------------------+ private const byte DynamicTableSizeUpdateMask = 0xe0; // http://httpwg.org/specs/rfc7541.html#rfc.section.5.2 // 0 1 2 3 4 5 6 7 // +---+---+---+---+---+---+---+---+ // | H | String Length (7+) | // +---+---------------------------+ private const byte HuffmanMask = 0x80; private const int IndexedHeaderFieldPrefix = 7; private const int LiteralHeaderFieldWithIncrementalIndexingPrefix = 6; private const int LiteralHeaderFieldWithoutIndexingPrefix = 4; private const int LiteralHeaderFieldNeverIndexedPrefix = 4; private const int DynamicTableSizeUpdatePrefix = 5; private const int StringLengthPrefix = 7; private readonly int _maxDynamicTableSize; private readonly int _maxHeadersLength; private readonly DynamicTable _dynamicTable; private IntegerDecoder _integerDecoder; private byte[] _stringOctets; private byte[] _headerNameOctets; private byte[] _headerValueOctets; private (int start, int length)? _headerNameRange; private (int start, int length)? _headerValueRange; private State _state = State.Ready; private byte[]? _headerName; private int _headerStaticIndex; private int _stringIndex; private int _stringLength; private int _headerNameLength; private int _headerValueLength; private bool _index; private bool _huffman; private bool _headersObserved; public HPackDecoder(int maxDynamicTableSize = DefaultHeaderTableSize, int maxHeadersLength = DefaultMaxHeadersLength) : this(maxDynamicTableSize, maxHeadersLength, new DynamicTable(maxDynamicTableSize)) { } // For testing. internal HPackDecoder(int maxDynamicTableSize, int maxHeadersLength, DynamicTable dynamicTable) { _maxDynamicTableSize = maxDynamicTableSize; _maxHeadersLength = maxHeadersLength; _dynamicTable = dynamicTable; _stringOctets = new byte[DefaultStringOctetsSize]; _headerNameOctets = new byte[DefaultStringOctetsSize]; _headerValueOctets = new byte[DefaultStringOctetsSize]; } public void Decode(in ReadOnlySequence<byte> data, bool endHeaders, IHttpStreamHeadersHandler handler) { foreach (ReadOnlyMemory<byte> segment in data) { DecodeInternal(segment.Span, handler); } CheckIncompleteHeaderBlock(endHeaders); } public void Decode(ReadOnlySpan<byte> data, bool endHeaders, IHttpStreamHeadersHandler handler) { DecodeInternal(data, handler); CheckIncompleteHeaderBlock(endHeaders); } private void DecodeInternal(ReadOnlySpan<byte> data, IHttpStreamHeadersHandler handler) { int currentIndex = 0; do { switch (_state) { case State.Ready: Parse(data, ref currentIndex, handler); break; case State.HeaderFieldIndex: ParseHeaderFieldIndex(data, ref currentIndex, handler); break; case State.HeaderNameIndex: ParseHeaderNameIndex(data, ref currentIndex, handler); break; case State.HeaderNameLength: ParseHeaderNameLength(data, ref currentIndex, handler); break; case State.HeaderNameLengthContinue: ParseHeaderNameLengthContinue(data, ref currentIndex, handler); break; case State.HeaderName: ParseHeaderName(data, ref currentIndex, handler); break; case State.HeaderValueLength: ParseHeaderValueLength(data, ref currentIndex, handler); break; case State.HeaderValueLengthContinue: ParseHeaderValueLengthContinue(data, ref currentIndex, handler); break; case State.HeaderValue: ParseHeaderValue(data, ref currentIndex, handler); break; case State.DynamicTableSizeUpdate: ParseDynamicTableSizeUpdate(data, ref currentIndex); break; default: // Can't happen Debug.Fail("HPACK decoder reach an invalid state"); throw new NotImplementedException(_state.ToString()); } } // Parse methods each check the length. This check is to see whether there is still data available // and to continue parsing. while (currentIndex < data.Length); // If a header range was set, but the value was not in the data, then copy the range // to the name buffer. Must copy because the data will be replaced and the range // will no longer be valid. if (_headerNameRange != null) { EnsureStringCapacity(ref _headerNameOctets, _headerNameLength); _headerName = _headerNameOctets; ReadOnlySpan<byte> headerBytes = data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length); headerBytes.CopyTo(_headerName); _headerNameRange = null; } } private void ParseDynamicTableSizeUpdate(ReadOnlySpan<byte> data, ref int currentIndex) { if (TryDecodeInteger(data, ref currentIndex, out int intResult)) { SetDynamicHeaderTableSize(intResult); _state = State.Ready; } } private void ParseHeaderValueLength(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { if (currentIndex < data.Length) { byte b = data[currentIndex++]; _huffman = IsHuffmanEncoded(b); if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out int intResult)) { OnStringLength(intResult, nextState: State.HeaderValue); if (intResult == 0) { OnString(nextState: State.Ready); ProcessHeaderValue(data, handler); } else { ParseHeaderValue(data, ref currentIndex, handler); } } else { _state = State.HeaderValueLengthContinue; ParseHeaderValueLengthContinue(data, ref currentIndex, handler); } } } private void ParseHeaderNameLengthContinue(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { if (TryDecodeInteger(data, ref currentIndex, out int intResult)) { // IntegerDecoder disallows overlong encodings, where an integer is encoded with more bytes than is strictly required. // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. Debug.Assert(intResult != 0, "A header name length of 0 should never be encoded with a continuation byte."); OnStringLength(intResult, nextState: State.HeaderName); ParseHeaderName(data, ref currentIndex, handler); } } private void ParseHeaderValueLengthContinue(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { if (TryDecodeInteger(data, ref currentIndex, out int intResult)) { // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. Debug.Assert(intResult != 0, "A header value length of 0 should never be encoded with a continuation byte."); OnStringLength(intResult, nextState: State.HeaderValue); ParseHeaderValue(data, ref currentIndex, handler); } } private void ParseHeaderFieldIndex(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { if (TryDecodeInteger(data, ref currentIndex, out int intResult)) { OnIndexedHeaderField(intResult, handler); } } private void ParseHeaderNameIndex(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { if (TryDecodeInteger(data, ref currentIndex, out int intResult)) { OnIndexedHeaderName(intResult); ParseHeaderValueLength(data, ref currentIndex, handler); } } private void ParseHeaderNameLength(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { if (currentIndex < data.Length) { byte b = data[currentIndex++]; _huffman = IsHuffmanEncoded(b); if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out int intResult)) { if (intResult == 0) { throw new HPackDecodingException(SR.Format(SR.net_http_invalid_header_name, "")); } OnStringLength(intResult, nextState: State.HeaderName); ParseHeaderName(data, ref currentIndex, handler); } else { _state = State.HeaderNameLengthContinue; ParseHeaderNameLengthContinue(data, ref currentIndex, handler); } } } private void Parse(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { if (currentIndex < data.Length) { Debug.Assert(_state == State.Ready, "Should be ready to parse a new header."); byte b = data[currentIndex++]; switch (BitOperations.LeadingZeroCount(b) - 24) // byte 'b' is extended to uint, so will have 24 extra 0s. { case 0: // Indexed Header Field { _headersObserved = true; int val = b & ~IndexedHeaderFieldMask; if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out int intResult)) { OnIndexedHeaderField(intResult, handler); } else { _state = State.HeaderFieldIndex; ParseHeaderFieldIndex(data, ref currentIndex, handler); } break; } case 1: // Literal Header Field with Incremental Indexing ParseLiteralHeaderField( data, ref currentIndex, b, LiteralHeaderFieldWithIncrementalIndexingMask, LiteralHeaderFieldWithIncrementalIndexingPrefix, index: true, handler); break; case 4: default: // Literal Header Field without Indexing ParseLiteralHeaderField( data, ref currentIndex, b, LiteralHeaderFieldWithoutIndexingMask, LiteralHeaderFieldWithoutIndexingPrefix, index: false, handler); break; case 3: // Literal Header Field Never Indexed ParseLiteralHeaderField( data, ref currentIndex, b, LiteralHeaderFieldNeverIndexedMask, LiteralHeaderFieldNeverIndexedPrefix, index: false, handler); break; case 2: // Dynamic Table Size Update { // https://tools.ietf.org/html/rfc7541#section-4.2 // This dynamic table size // update MUST occur at the beginning of the first header block // following the change to the dynamic table size. if (_headersObserved) { throw new HPackDecodingException(SR.net_http_hpack_late_dynamic_table_size_update); } if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out int intResult)) { SetDynamicHeaderTableSize(intResult); } else { _state = State.DynamicTableSizeUpdate; ParseDynamicTableSizeUpdate(data, ref currentIndex); } break; } } } } private void ParseLiteralHeaderField(ReadOnlySpan<byte> data, ref int currentIndex, byte b, byte mask, byte indexPrefix, bool index, IHttpStreamHeadersHandler handler) { _headersObserved = true; _index = index; int val = b & ~mask; if (val == 0) { _state = State.HeaderNameLength; ParseHeaderNameLength(data, ref currentIndex, handler); } else { if (_integerDecoder.BeginTryDecode((byte)val, indexPrefix, out int intResult)) { OnIndexedHeaderName(intResult); ParseHeaderValueLength(data, ref currentIndex, handler); } else { _state = State.HeaderNameIndex; ParseHeaderNameIndex(data, ref currentIndex, handler); } } } private void ParseHeaderName(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { // Read remaining chars, up to the length of the current data int count = Math.Min(_stringLength - _stringIndex, data.Length - currentIndex); // Check whether the whole string is available in the data and no decompression required. // If string is good then mark its range. // NOTE: it may need to be copied to buffer later the if value is not current data. if (count == _stringLength && !_huffman) { // Fast path. Store the range rather than copying. _headerNameRange = (start: currentIndex, count); _headerNameLength = _stringLength; currentIndex += count; _state = State.HeaderValueLength; ParseHeaderValueLength(data, ref currentIndex, handler); } else if (count == 0) { // no-op } else { // Copy string to temporary buffer. // _stringOctets was already data.Slice(currentIndex, count).CopyTo(_stringOctets.AsSpan(_stringIndex)); _stringIndex += count; currentIndex += count; if (_stringIndex == _stringLength) { OnString(nextState: State.HeaderValueLength); ParseHeaderValueLength(data, ref currentIndex, handler); } } } private void ParseHeaderValue(ReadOnlySpan<byte> data, ref int currentIndex, IHttpStreamHeadersHandler handler) { // Read remaining chars, up to the length of the current data int count = Math.Min(_stringLength - _stringIndex, data.Length - currentIndex); // Check whether the whole string is available in the data and no decompressed required. // If string is good then mark its range. if (count == _stringLength && !_huffman) { // Fast path. Store the range rather than copying. _headerValueRange = (start: currentIndex, count); currentIndex += count; _state = State.Ready; ProcessHeaderValue(data, handler); } else { // Copy string to temporary buffer. data.Slice(currentIndex, count).CopyTo(_stringOctets.AsSpan(_stringIndex)); _stringIndex += count; currentIndex += count; if (_stringIndex == _stringLength) { OnString(nextState: State.Ready); ProcessHeaderValue(data, handler); } } } private void CheckIncompleteHeaderBlock(bool endHeaders) { if (endHeaders) { if (_state != State.Ready) { throw new HPackDecodingException(SR.net_http_hpack_incomplete_header_block); } _headersObserved = false; } } private void ProcessHeaderValue(ReadOnlySpan<byte> data, IHttpStreamHeadersHandler handler) { ReadOnlySpan<byte> headerValueSpan = _headerValueRange == null ? _headerValueOctets.AsSpan(0, _headerValueLength) : data.Slice(_headerValueRange.GetValueOrDefault().start, _headerValueRange.GetValueOrDefault().length); if (_headerStaticIndex > 0) { handler.OnStaticIndexedHeader(_headerStaticIndex, headerValueSpan); if (_index) { _dynamicTable.Insert(_headerStaticIndex, H2StaticTable.Get(_headerStaticIndex - 1).Name, headerValueSpan); } } else { ReadOnlySpan<byte> headerNameSpan = _headerNameRange == null ? _headerName.AsSpan(0, _headerNameLength) : data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length); handler.OnHeader(headerNameSpan, headerValueSpan); if (_index) { _dynamicTable.Insert(headerNameSpan, headerValueSpan); } } _headerStaticIndex = 0; _headerNameRange = null; _headerValueRange = null; } public void CompleteDecode() { if (_state != State.Ready) { // Incomplete header block throw new HPackDecodingException(SR.net_http_hpack_unexpected_end); } } private void OnIndexedHeaderField(int index, IHttpStreamHeadersHandler handler) { if (index <= H2StaticTable.Count) { handler.OnStaticIndexedHeader(index); } else { ref readonly HeaderField header = ref GetDynamicHeader(index); handler.OnDynamicIndexedHeader(header.StaticTableIndex, header.Name, header.Value); } _state = State.Ready; } private void OnIndexedHeaderName(int index) { if (index <= H2StaticTable.Count) { _headerStaticIndex = index; } else { _headerName = GetDynamicHeader(index).Name; _headerNameLength = _headerName.Length; } _state = State.HeaderValueLength; } private void OnStringLength(int length, State nextState) { if (length > _stringOctets.Length) { if (length > _maxHeadersLength) { throw new HPackDecodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxHeadersLength)); } _stringOctets = new byte[Math.Max(length, Math.Min(_stringOctets.Length * 2, _maxHeadersLength))]; } _stringLength = length; _stringIndex = 0; _state = nextState; } private void OnString(State nextState) { int Decode(ref byte[] dst) { if (_huffman) { int decodedLength = Huffman.Decode(new ReadOnlySpan<byte>(_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; } } try { if (_state == State.HeaderName) { _headerNameLength = Decode(ref _headerNameOctets); _headerName = _headerNameOctets; } else { _headerValueLength = Decode(ref _headerValueOctets); } } catch (HuffmanDecodingException ex) { throw new HPackDecodingException(SR.net_http_hpack_huffman_decode_failed, ex); } _state = nextState; } private void EnsureStringCapacity(ref byte[] dst, int stringLength = -1) { stringLength = stringLength >= 0 ? stringLength : _stringLength; if (dst.Length < stringLength) { dst = new byte[Math.Max(stringLength, Math.Min(dst.Length * 2, _maxHeadersLength))]; } } private bool TryDecodeInteger(ReadOnlySpan<byte> data, ref int currentIndex, out int result) { for (; currentIndex < data.Length; currentIndex++) { if (_integerDecoder.TryDecode(data[currentIndex], out result)) { currentIndex++; return true; } } result = default; return false; } private static bool IsHuffmanEncoded(byte b) { return (b & HuffmanMask) != 0; } private ref readonly HeaderField GetDynamicHeader(int index) { try { return ref _dynamicTable[index - H2StaticTable.Count - 1]; } catch (IndexOutOfRangeException) { // Header index out of range. throw new HPackDecodingException(SR.Format(SR.net_http_hpack_invalid_index, index)); } } private void SetDynamicHeaderTableSize(int size) { if (size > _maxDynamicTableSize) { throw new HPackDecodingException(SR.Format(SR.net_http_hpack_large_table_size_update, size, _maxDynamicTableSize)); } _dynamicTable.UpdateMaxSize(size); } } }