diff options
author | Hao Kung <HaoK@users.noreply.github.com> | 2022-03-11 19:51:57 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-11 19:51:57 +0300 |
commit | ac41f6340fe9121168aba10e6fb7006f293f31fd (patch) | |
tree | 5b53895d07cecf95dccd6553af5699e52fc5bc7b | |
parent | ca15c1b691e44c35767212d96d0402d33e50f1a9 (diff) | |
parent | 7ea70a5218cb7ab4cf0d836bd39df4904d50e813 (diff) |
Merge branch 'main' into haok/r2haok/r2
14 files changed, 95 insertions, 15 deletions
diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs index 76d8f09d55..40ae03126c 100644 --- a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs +++ b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Components.Reflection; @@ -15,7 +16,7 @@ internal sealed class QueryParameterValueSupplier { public static void ClearCache() => _cacheByType.Clear(); - private static readonly Dictionary<Type, QueryParameterValueSupplier?> _cacheByType = new(); + private static readonly ConcurrentDictionary<Type, QueryParameterValueSupplier?> _cacheByType = new(); // These two arrays contain the same number of entries, and their corresponding positions refer to each other. // Holding the info like this means we can use Array.BinarySearch with less custom implementation. diff --git a/src/Http/WebUtilities/src/FormPipeReader.cs b/src/Http/WebUtilities/src/FormPipeReader.cs index fb7325349a..845ad7bf6b 100644 --- a/src/Http/WebUtilities/src/FormPipeReader.cs +++ b/src/Http/WebUtilities/src/FormPipeReader.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Globalization; using System.IO.Pipelines; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -249,8 +250,8 @@ public class FormPipeReader { if (!isFinalBlock) { - // Don't buffer indefinitely - if ((uint)(sequenceReader.Consumed - consumedBytes) > (uint)KeyLengthLimit + (uint)ValueLengthLimit) + // +2 to account for '&' and '=' + if ((sequenceReader.Length - consumedBytes) > (long)KeyLengthLimit + (long)ValueLengthLimit + 2) { ThrowKeyOrValueTooLargeException(); } @@ -314,17 +315,30 @@ public class FormPipeReader private void ThrowKeyOrValueTooLargeException() { - throw new InvalidDataException($"Form key length limit {KeyLengthLimit} or value length limit {ValueLengthLimit} exceeded."); + throw new InvalidDataException( + string.Format( + CultureInfo.CurrentCulture, + Resources.FormPipeReader_KeyOrValueTooLarge, + KeyLengthLimit, + ValueLengthLimit)); } private void ThrowKeyTooLargeException() { - throw new InvalidDataException($"Form key length limit {KeyLengthLimit} exceeded."); + throw new InvalidDataException( + string.Format( + CultureInfo.CurrentCulture, + Resources.FormPipeReader_KeyTooLarge, + KeyLengthLimit)); } private void ThrowValueTooLargeException() { - throw new InvalidDataException($"Form value length limit {ValueLengthLimit} exceeded."); + throw new InvalidDataException( + string.Format( + CultureInfo.CurrentCulture, + Resources.FormPipeReader_ValueTooLarge, + ValueLengthLimit)); } [SkipLocalsInit] diff --git a/src/Http/WebUtilities/src/Resources.resx b/src/Http/WebUtilities/src/Resources.resx index a32d2db5cc..d055be3bad 100644 --- a/src/Http/WebUtilities/src/Resources.resx +++ b/src/Http/WebUtilities/src/Resources.resx @@ -117,6 +117,15 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> + <data name="FormPipeReader_KeyOrValueTooLarge" xml:space="preserve"> + <value>Form key length limit {0} or value length limit {1} exceeded.</value> + </data> + <data name="FormPipeReader_KeyTooLarge" xml:space="preserve"> + <value>Form key length limit {0} exceeded.</value> + </data> + <data name="FormPipeReader_ValueTooLarge" xml:space="preserve"> + <value>Form value length limit {0} exceeded.</value> + </data> <data name="HttpRequestStreamReader_StreamNotReadable" xml:space="preserve"> <value>The stream must support reading.</value> </data> diff --git a/src/Http/WebUtilities/test/FormPipeReaderTests.cs b/src/Http/WebUtilities/test/FormPipeReaderTests.cs index 1b3b88c594..dac33ec25a 100644 --- a/src/Http/WebUtilities/test/FormPipeReaderTests.cs +++ b/src/Http/WebUtilities/test/FormPipeReaderTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Globalization; using System.IO.Pipelines; using System.Text; using Microsoft.Extensions.Primitives; @@ -209,6 +210,30 @@ public class FormPipeReaderTests Assert.Equal(Encoding.UTF8.GetBytes("baz=12345678901"), readResult.Buffer.ToArray()); } + [Fact] + public void ReadFormAsync_ChunkedDataNoDelimiter_ThrowsEarly() + { + var bytes = CreateBytes_NoDelimiter(10 * 1024); + var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(bytes); + KeyValueAccumulator accumulator = default; + var valueLengthLimit = 1024; + var keyLengthLimit = 10; + var formReader = new FormPipeReader(null!) + { + ValueLengthLimit = valueLengthLimit, + KeyLengthLimit = keyLengthLimit + }; + var exception = Assert.Throws<InvalidDataException>( + () => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: false)); + // Make sure that FormPipeReader throws an exception after hitting KeyLengthLimit + ValueLengthLimit, + // Rather than after reading the entire request. + Assert.Equal(string.Format( + CultureInfo.CurrentCulture, + Resources.FormPipeReader_KeyOrValueTooLarge, + keyLengthLimit, + valueLengthLimit), exception.Message); + } + // https://en.wikipedia.org/wiki/Percent-encoding [Theory] [InlineData("++=hello", " ", "hello")] @@ -610,4 +635,16 @@ public class FormPipeReaderTests bodyPipe.Writer.Complete(); return bodyPipe.Reader; } + + private static byte[] CreateBytes_NoDelimiter(int n) + { + //Create the bytes of "key=vvvvvvvv....", of length n + var keyValue = new char[n]; + Array.Fill(keyValue, 'v'); + keyValue[0] = 'k'; + keyValue[1] = 'e'; + keyValue[2] = 'y'; + keyValue[3] = '='; + return Encoding.UTF8.GetBytes(keyValue); + } } diff --git a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj index dff0c5861c..07f2d99eed 100644 --- a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj +++ b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj @@ -18,6 +18,7 @@ <Compile Include="$(SharedSourceRoot)HttpSys\**\*.cs" /> <Compile Include="$(SharedSourceRoot)Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" /> <Compile Include="$(SharedSourceRoot)ServerInfrastructure\StringUtilities.cs" LinkBase="ServerInfrastructure\StringUtilities.cs" /> + <Compile Include="$(SharedSourceRoot)ServerInfrastructure\HttpCharacters.cs" LinkBase="ServerInfrastructure\HttpCharacters.cs" /> <Compile Include="$(SharedSourceRoot)TaskToApm.cs" /> </ItemGroup> diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 230cbc3f90..64762913a8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO.Pipelines; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs index 95f7e72378..5d6682cf71 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs @@ -8,7 +8,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index 80ec7fd95b..658990cc85 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 9c94e933bd..54f4e14a0c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Primitives; +using HttpCharacters = Microsoft.AspNetCore.Http.HttpCharacters; using HttpMethods = Microsoft.AspNetCore.Http.HttpMethods; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 35a272cfcc..462e56533e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -20,6 +20,7 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; using HttpMethods = Microsoft.AspNetCore.Http.HttpMethods; +using HttpCharacters = Microsoft.AspNetCore.Http.HttpCharacters; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index 578c0310d4..8a37f6d6d2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; diff --git a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs index 893a1fd3ed..aaca50f66e 100644 --- a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs @@ -159,6 +159,18 @@ public class HttpResponseHeadersTests }); } + [Fact] + public void AddingTabCharactersToHeaderPropertyWorks() + { + var responseHeaders = (IHeaderDictionary)new HttpResponseHeaders(); + + // Known special header + responseHeaders.Allow = "Da\tta"; + + // Unknown header fallback + responseHeaders.Accept = "Da\tta"; + } + [Theory] [InlineData("\r\nData")] [InlineData("\0Data")] diff --git a/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs b/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs index 687e55921f..cfcf13ae2a 100644 --- a/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs +++ b/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs @@ -272,12 +272,10 @@ internal class HeaderCollection : IHeaderDictionary { if (headerCharacters != null) { - foreach (var ch in headerCharacters) + var invalid = HttpCharacters.IndexOfInvalidFieldValueChar(headerCharacters); + if (invalid >= 0) { - if (ch < 0x20) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Invalid control character in header: 0x{0:X2}", (byte)ch)); - } + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Invalid control character in header: 0x{0:X2}", headerCharacters[invalid])); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs b/src/Shared/ServerInfrastructure/HttpCharacters.cs index d344dabeda..9892ed6fdb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs +++ b/src/Shared/ServerInfrastructure/HttpCharacters.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +namespace Microsoft.AspNetCore.Http; internal static class HttpCharacters { @@ -107,6 +107,9 @@ internal static class HttpCharacters { // field-value https://tools.ietf.org/html/rfc7230#section-3.2 var fieldValue = new bool[_tableSize]; + + fieldValue[0x9] = true; // HTAB + for (var c = 0x20; c <= 0x7e; c++) // VCHAR and SP { fieldValue[c] = true; @@ -182,7 +185,8 @@ internal static class HttpCharacters return -1; } - // Disallows control characters and anything more than 0x7F + // Follows field-value rules in https://tools.ietf.org/html/rfc7230#section-3.2 + // Disallows characters > 0x7E. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int IndexOfInvalidFieldValueChar(string s) { @@ -200,7 +204,7 @@ internal static class HttpCharacters return -1; } - // Disallows control characters but allows extended characters > 0x7F + // Follows field-value rules for chars <= 0x7F. Allows extended characters > 0x7F. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int IndexOfInvalidFieldValueCharExtended(string s) { |