Skip to content

Commit 7ebbf76

Browse files
committed
Merged PR 20757: [5.0] MSRC 69432 - ASP.NET Core - Denial of service in ASP.NET Core via FormPipeReader
# [5.0] MSRC 69432 - ASP.NET Core - Denial of service in ASP.NET Core via FormPipeReader Fixes a bug in FormPipeReader where data without a delimiter will be buffered indefinitely, beyond configured limits. ## Description When chunked data without a delimiter is sent to FormPipeReader, FormPipeReader will read the entire stream of data, starting from the beginning each time, without honoring configured length limits. This is because, after each read, it checks if `SequenceReader.Consumed` is greater than the configured limit, but `SequenceReader.Consumed` is 0 when no delimiter was found. Therefore the check against the length limit is never honored, and we continue to read data indefinitely, starting from the beginning of the stream each time. Also brings in the changes from dotnet#27586 ## Customer Impact Potential Denial-Of-Service attack on services using FormPipeReader ## Regression? - [ ] Yes - [ x ] No [If yes, specify the version the behavior has regressed from] ## Risk - [ ] High - [ x ] Medium - [ ] Low The fix is a one-liner, and tests confirm a significant positive improvement on perf. There could be orthogonal issues that we've missed ## Verification - [ x ] Manual (required) - [ x ] Automated ## Packaging changes reviewed? - [ x ] Yes - [ ] No - [ ] N/A ---- ## When servicing release/2.1 - [ ] Make necessary changes in eng/PatchConfig.props FormPipeReader
1 parent 57fc9c5 commit 7ebbf76

File tree

4 files changed

+99
-33
lines changed

4 files changed

+99
-33
lines changed

src/Http/WebUtilities/src/FormPipeReader.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Buffers;
66
using System.Collections.Generic;
77
using System.Diagnostics;
8+
using System.Globalization;
89
using System.IO;
910
using System.IO.Pipelines;
1011
using System.Runtime.CompilerServices;
@@ -101,7 +102,7 @@ public async Task<Dictionary<string, StringValues>> ReadFormAsync(CancellationTo
101102
}
102103
catch
103104
{
104-
_pipeReader.AdvanceTo(buffer.Start);
105+
_pipeReader.AdvanceTo(buffer.Start, buffer.End);
105106
throw;
106107
}
107108
}
@@ -247,7 +248,8 @@ private void ParseValuesSlow(
247248
if (!isFinalBlock)
248249
{
249250
// Don't buffer indefinitely
250-
if ((uint)(sequenceReader.Consumed - consumedBytes) > (uint)KeyLengthLimit + (uint)ValueLengthLimit)
251+
// +2 to account for '&' and '='
252+
if ((sequenceReader.Length - consumedBytes) > (long)KeyLengthLimit + (long)ValueLengthLimit + 2)
251253
{
252254
ThrowKeyOrValueTooLargeException();
253255
}
@@ -311,17 +313,30 @@ private void ParseValuesSlow(
311313

312314
private void ThrowKeyOrValueTooLargeException()
313315
{
314-
throw new InvalidDataException($"Form key length limit {KeyLengthLimit} or value length limit {ValueLengthLimit} exceeded.");
316+
throw new InvalidDataException(
317+
string.Format(
318+
CultureInfo.CurrentCulture,
319+
Resources.FormPipeReader_KeyOrValueTooLarge,
320+
KeyLengthLimit,
321+
ValueLengthLimit));
315322
}
316323

317324
private void ThrowKeyTooLargeException()
318325
{
319-
throw new InvalidDataException($"Form key length limit {KeyLengthLimit} exceeded.");
326+
throw new InvalidDataException(
327+
string.Format(
328+
CultureInfo.CurrentCulture,
329+
Resources.FormPipeReader_KeyTooLarge,
330+
KeyLengthLimit));
320331
}
321332

322333
private void ThrowValueTooLargeException()
323334
{
324-
throw new InvalidDataException($"Form value length limit {ValueLengthLimit} exceeded.");
335+
throw new InvalidDataException(
336+
string.Format(
337+
CultureInfo.CurrentCulture,
338+
Resources.FormPipeReader_ValueTooLarge,
339+
ValueLengthLimit));
325340
}
326341

327342
private string GetDecodedStringFromReadOnlySequence(in ReadOnlySequence<byte> ros)

src/Http/WebUtilities/src/Resources.resx

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -117,6 +117,15 @@
117117
<resheader name="writer">
118118
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119119
</resheader>
120+
<data name="FormPipeReader_KeyOrValueTooLarge" xml:space="preserve">
121+
<value>Form key length limit {0} or value length limit {1} exceeded.</value>
122+
</data>
123+
<data name="FormPipeReader_KeyTooLarge" xml:space="preserve">
124+
<value>Form key length limit {0} exceeded.</value>
125+
</data>
126+
<data name="FormPipeReader_ValueTooLarge" xml:space="preserve">
127+
<value>Form value length limit {0} exceeded.</value>
128+
</data>
120129
<data name="HttpRequestStreamReader_StreamNotReadable" xml:space="preserve">
121130
<value>The stream must support reading.</value>
122131
</data>

src/Http/WebUtilities/test/FormPipeReaderTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System;
55
using System.Buffers;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
8+
using System.Globalization;
79
using System.IO;
810
using System.IO.Pipelines;
911
using System.Text;
@@ -190,6 +192,34 @@ public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw()
190192
Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray());
191193
}
192194

195+
[Fact]
196+
public void ReadFormAsync_ChunkedDataNoDelimiter_ThrowsEarly()
197+
{
198+
byte[] bytes = CreateBytes_NoDelimiter((10 * 1024) + 2);
199+
var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(bytes);
200+
201+
KeyValueAccumulator accumulator = default;
202+
203+
var valueLengthLimit = 1024;
204+
var keyLengthLimit = 10;
205+
206+
var formReader = new FormPipeReader(null!)
207+
{
208+
ValueLengthLimit = valueLengthLimit,
209+
KeyLengthLimit = keyLengthLimit
210+
};
211+
212+
var exception = Assert.Throws<InvalidDataException>(
213+
() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: false));
214+
// Make sure that FormPipeReader throws an exception after hitting KeyLengthLimit + ValueLengthLimit,
215+
// Rather than after reading the entire request.
216+
Assert.Equal(string.Format(
217+
CultureInfo.CurrentCulture,
218+
Resources.FormPipeReader_KeyOrValueTooLarge,
219+
keyLengthLimit,
220+
valueLengthLimit), exception.Message);
221+
}
222+
193223
// https://en.wikipedia.org/wiki/Percent-encoding
194224
[Theory]
195225
[InlineData("++=hello", " ", "hello")]
@@ -581,5 +611,17 @@ private static async Task<PipeReader> MakePipeReader(string text)
581611
bodyPipe.Writer.Complete();
582612
return bodyPipe.Reader;
583613
}
614+
615+
private static byte[] CreateBytes_NoDelimiter(int n)
616+
{
617+
//Create the bytes of "key=vvvvvvvv....", of length n
618+
var keyValue = new char[n];
619+
Array.Fill(keyValue, 'v');
620+
keyValue[0] = 'k';
621+
keyValue[1] = 'e';
622+
keyValue[2] = 'y';
623+
keyValue[3] = '=';
624+
return Encoding.UTF8.GetBytes(keyValue);
625+
}
584626
}
585627
}

src/Http/WebUtilities/test/FormReaderTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,4 @@ private static Stream MakeStream(bool bufferRequest, string text)
227227
return body;
228228
}
229229
}
230-
}
230+
}

0 commit comments

Comments
 (0)