From f56b682b364ebf12cb1779b0110536f8b2648bfc Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 24 Sep 2018 10:06:51 -0700 Subject: [PATCH] Impose integer decode limit in HPACK --- .../IntegerDecoderBenchmark.cs | 41 +++++++++++ src/Kestrel.Core/CoreStrings.resx | 57 +++++++-------- .../Internal/Http2/HPack/HPackDecoder.cs | 53 +++++++------- .../Internal/Http2/HPack/IntegerDecoder.cs | 69 +++++++++++++------ .../Properties/CoreStrings.Designer.cs | 14 ++++ test/Kestrel.Core.Tests/HPackIntegerTests.cs | 39 +++++++++++ .../Kestrel.Core.Tests/IntegerDecoderTests.cs | 43 ++++++++++-- .../Http2/Http2ConnectionTests.cs | 28 ++++++++ 8 files changed, 265 insertions(+), 79 deletions(-) create mode 100644 benchmarks/Kestrel.Performance/IntegerDecoderBenchmark.cs create mode 100644 test/Kestrel.Core.Tests/HPackIntegerTests.cs diff --git a/benchmarks/Kestrel.Performance/IntegerDecoderBenchmark.cs b/benchmarks/Kestrel.Performance/IntegerDecoderBenchmark.cs new file mode 100644 index 0000000000..6fc22390c0 --- /dev/null +++ b/benchmarks/Kestrel.Performance/IntegerDecoderBenchmark.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class IntegerDecoderBenchmark + { + private const int Iterations = 50_000; + + private int _prefixLength = 5; // Arbitrary prefix length + private byte _singleByte = 0x1e; // 30 + private byte[] _multiByte = new byte[] { 0x1f, 0xe0, 0xff, 0xff, 0xff, 0x03}; // int32.MaxValue + private IntegerDecoder _integerDecoder = new IntegerDecoder(); + + [Benchmark(Baseline = true, OperationsPerInvoke = Iterations)] + public void DecodeSingleByteInteger() + { + for (var i = 0; i < Iterations; i++) + { + _integerDecoder.BeginTryDecode(_singleByte, _prefixLength, out _); + } + } + + [Benchmark(OperationsPerInvoke = Iterations)] + public void DecodeMultiByteInteger() + { + for (var i = 0; i < Iterations; i++) + { + _integerDecoder.BeginTryDecode(_multiByte[0], _prefixLength, out _); + + for (var j = 1; j < _multiByte.Length; j++) + { + _integerDecoder.TryDecode(_multiByte[j], out _); + } + } + } + } +} diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 5a04c01d0c..e3b66fe44a 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -581,4 +581,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l The given buffer was too small to encode any headers. + + The decoded integer exceeds the maximum value of Int32.MaxValue. + \ No newline at end of file diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs index e88a4e077b..3decfb4c08 100644 --- a/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs +++ b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs @@ -132,6 +132,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack private void OnByte(byte b, IHttpHeadersHandler handler) { + int intResult; switch (_state) { case State.Ready: @@ -140,9 +141,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack _headersObserved = true; var val = b & ~IndexedHeaderFieldMask; - if (_integerDecoder.BeginDecode((byte)val, IndexedHeaderFieldPrefix)) + if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult)) { - OnIndexedHeaderField(_integerDecoder.Value, handler); + OnIndexedHeaderField(intResult, handler); } else { @@ -159,9 +160,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack { _state = State.HeaderNameLength; } - else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix)) + else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult)) { - OnIndexedHeaderName(_integerDecoder.Value); + OnIndexedHeaderName(intResult); } else { @@ -178,9 +179,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack { _state = State.HeaderNameLength; } - else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix)) + else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult)) { - OnIndexedHeaderName(_integerDecoder.Value); + OnIndexedHeaderName(intResult); } else { @@ -197,9 +198,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack { _state = State.HeaderNameLength; } - else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix)) + else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix, out intResult)) { - OnIndexedHeaderName(_integerDecoder.Value); + OnIndexedHeaderName(intResult); } else { @@ -217,9 +218,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack throw new HPackDecodingException(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock); } - if (_integerDecoder.BeginDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix)) + if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out intResult)) { - SetDynamicHeaderTableSize(_integerDecoder.Value); + SetDynamicHeaderTableSize(intResult); } else { @@ -234,25 +235,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack break; case State.HeaderFieldIndex: - if (_integerDecoder.Decode(b)) + if (_integerDecoder.TryDecode(b, out intResult)) { - OnIndexedHeaderField(_integerDecoder.Value, handler); + OnIndexedHeaderField(intResult, handler); } break; case State.HeaderNameIndex: - if (_integerDecoder.Decode(b)) + if (_integerDecoder.TryDecode(b, out intResult)) { - OnIndexedHeaderName(_integerDecoder.Value); + OnIndexedHeaderName(intResult); } break; case State.HeaderNameLength: _huffman = (b & HuffmanMask) != 0; - if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix)) + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) { - OnStringLength(_integerDecoder.Value, nextState: State.HeaderName); + OnStringLength(intResult, nextState: State.HeaderName); } else { @@ -261,9 +262,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack break; case State.HeaderNameLengthContinue: - if (_integerDecoder.Decode(b)) + if (_integerDecoder.TryDecode(b, out intResult)) { - OnStringLength(_integerDecoder.Value, nextState: State.HeaderName); + OnStringLength(intResult, nextState: State.HeaderName); } break; @@ -279,10 +280,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack case State.HeaderValueLength: _huffman = (b & HuffmanMask) != 0; - if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix)) + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) { - OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue); - if (_integerDecoder.Value == 0) + OnStringLength(intResult, nextState: State.HeaderValue); + if (intResult == 0) { ProcessHeaderValue(handler); } @@ -294,10 +295,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack break; case State.HeaderValueLengthContinue: - if (_integerDecoder.Decode(b)) + if (_integerDecoder.TryDecode(b, out intResult)) { - OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue); - if (_integerDecoder.Value == 0) + OnStringLength(intResult, nextState: State.HeaderValue); + if (intResult == 0) { ProcessHeaderValue(handler); } @@ -314,9 +315,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack break; case State.DynamicTableSizeUpdate: - if (_integerDecoder.Decode(b)) + if (_integerDecoder.TryDecode(b, out intResult)) { - SetDynamicHeaderTableSize(_integerDecoder.Value); + SetDynamicHeaderTableSize(intResult); _state = State.Ready; } diff --git a/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs b/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs index 5bc051a9a3..4805993b6f 100644 --- a/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs +++ b/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs @@ -3,40 +3,65 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack { + /// + /// The maximum we will decode is Int32.MaxValue, which is also the maximum request header field size. + /// public class IntegerDecoder { private int _i; private int _m; - public int Value { get; private set; } - - public bool BeginDecode(byte b, int prefixLength) + /// + /// Callers must ensure higher bits above the prefix are cleared before calling this method. + /// + /// + /// + /// + /// + public bool BeginTryDecode(byte b, int prefixLength, out int result) { if (b < ((1 << prefixLength) - 1)) { - Value = b; - return true; - } - else - { - _i = b; - _m = 0; - return false; - } - } - - public bool Decode(byte b) - { - _i = _i + (b & 127) * (1 << _m); - _m = _m + 7; - - if ((b & 128) != 128) - { - Value = _i; + result = b; return true; } + _i = b; + _m = 0; + result = 0; return false; } + + public bool TryDecode(byte b, out int result) + { + var m = _m; // Enregister + var i = _i + ((b & 0x7f) << m); // Enregister + + if ((b & 0x80) == 0) + { + // Int32.MaxValue only needs a maximum of 5 bytes to represent and the last byte cannot have any value set larger than 0x7 + if ((m > 21 && b > 0x7) || i < 0) + { + ThrowIntegerTooBigException(); + } + + result = i; + return true; + } + else if (m > 21) + { + // Int32.MaxValue only needs a maximum of 5 bytes to represent + ThrowIntegerTooBigException(); + } + + _m = m + 7; + _i = i; + + result = 0; + return false; + } + + public static void ThrowIntegerTooBigException() + => throw new HPackDecodingException(CoreStrings.HPackErrorIntegerTooBig); } } diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index 4717a70da3..51388a5b88 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -2170,6 +2170,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHPackErrorNotEnoughBuffer() => GetString("HPackErrorNotEnoughBuffer"); + /// + /// The decoded integer exceeds the maximum value of Int32.MaxValue. + /// + internal static string HPackErrorIntegerTooBig + { + get => GetString("HPackErrorIntegerTooBig"); + } + + /// + /// The decoded integer exceeds the maximum value of Int32.MaxValue. + /// + internal static string FormatHPackErrorIntegerTooBig() + => GetString("HPackErrorIntegerTooBig"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/test/Kestrel.Core.Tests/HPackIntegerTests.cs b/test/Kestrel.Core.Tests/HPackIntegerTests.cs new file mode 100644 index 0000000000..06cd800a7f --- /dev/null +++ b/test/Kestrel.Core.Tests/HPackIntegerTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class HPackIntegerTests + { + [Fact] + public void IntegerEncoderDecoderRoundtrips() + { + var decoder = new IntegerDecoder(); + var range = 1 << 8; + + foreach (var i in Enumerable.Range(0, range).Concat(Enumerable.Range(int.MaxValue - range + 1, range))) + { + for (int n = 1; n <= 8; n++) + { + var integerBytes = new byte[6]; + Assert.True(IntegerEncoder.Encode(i, n, integerBytes, out var length)); + + var decodeResult = decoder.BeginTryDecode(integerBytes[0], n, out var intResult); + + for (int j = 1; j < length; j++) + { + Assert.False(decodeResult); + decodeResult = decoder.TryDecode(integerBytes[j], out intResult); + } + + Assert.True(decodeResult); + Assert.Equal(i, intResult); + } + } + } + } +} diff --git a/test/Kestrel.Core.Tests/IntegerDecoderTests.cs b/test/Kestrel.Core.Tests/IntegerDecoderTests.cs index b89b0f84a3..fdebb56922 100644 --- a/test/Kestrel.Core.Tests/IntegerDecoderTests.cs +++ b/test/Kestrel.Core.Tests/IntegerDecoderTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public void IntegerDecode(int i, int prefixLength, byte[] octets) { var decoder = new IntegerDecoder(); - var result = decoder.BeginDecode(octets[0], prefixLength); + var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult); if (octets.Length == 1) { @@ -25,13 +25,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests for (; j < octets.Length - 1; j++) { - Assert.False(decoder.Decode(octets[j])); + Assert.False(decoder.TryDecode(octets[j], out intResult)); } - Assert.True(decoder.Decode(octets[j])); + Assert.True(decoder.TryDecode(octets[j], out intResult)); } - Assert.Equal(i, decoder.Value); + Assert.Equal(i, intResult); + } + + [Theory] + [MemberData(nameof(IntegerData_OverMax))] + public void IntegerDecode_Throws_IfMaxExceeded(int prefixLength, byte[] octets) + { + var decoder = new IntegerDecoder(); + var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult); + + for (var j = 1; j < octets.Length - 1; j++) + { + Assert.False(decoder.TryDecode(octets[j], out intResult)); + } + + Assert.Throws(() => decoder.TryDecode(octets[octets.Length - 1], out intResult)); } public static TheoryData IntegerData @@ -43,6 +58,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests data.Add(10, 5, new byte[] { 10 }); data.Add(1337, 5, new byte[] { 0x1f, 0x9a, 0x0a }); data.Add(42, 8, new byte[] { 42 }); + data.Add(7, 3, new byte[] { 0x7, 0x0 }); + data.Add(int.MaxValue, 1, new byte[] { 0x01, 0xfe, 0xff, 0xff, 0xff, 0x07 }); + data.Add(int.MaxValue, 8, new byte[] { 0xff, 0x80, 0xfe, 0xff, 0xff, 0x07 }); + + return data; + } + } + + public static TheoryData IntegerData_OverMax + { + get + { + var data = new TheoryData(); + + data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1 + data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x08 }); // MSB exceeds maximum + data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set + data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1 + data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x08 }); // MSB exceeds maximum + data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set return data; } diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index ef2be68ae4..c0ec666f62 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -1527,6 +1527,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock); } + [Fact] + public async Task HEADERS_Received_IntegerOverLimit_ConnectionError() + { + await InitializeConnectionAsync(_noopApplication); + + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); + + frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, 1); + frame.PayloadLength = 7; + var payload = new byte[] + { + // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, + 0x00, + // with an name of size that's greater than int.MaxValue + 0x7f, 0x80, 0x80, 0x80, 0x80, 0x7f + }; + + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(payload); + + await WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: false, + expectedLastStreamId: 1, + expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, + expectedErrorMessage: CoreStrings.HPackErrorIntegerTooBig); + } + [Theory] [MemberData(nameof(IllegalTrailerData))] public async Task HEADERS_Received_WithTrailers_ContainsIllegalTrailer_ConnectionError(byte[] trailers, string expectedErrorMessage)