Skip to content

Commit 7c9bd7b

Browse files
committed
Merged PR 20748: [6.0] MSRC 69432 - ASP.NET Core - Denial of service in ASP.NET Core via FormPipeReader
# [6.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. ## 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 7cdac65 commit 7c9bd7b

File tree

3 files changed

+96
-31
lines changed

3 files changed

+96
-31
lines changed

src/Http/WebUtilities/src/FormPipeReader.cs

Lines changed: 19 additions & 4 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;
@@ -255,7 +256,8 @@ private void ParseValuesSlow(
255256
if (!isFinalBlock)
256257
{
257258
// Don't buffer indefinitely
258-
if ((uint)(sequenceReader.Consumed - consumedBytes) > (uint)KeyLengthLimit + (uint)ValueLengthLimit)
259+
// +2 to account for '&' and '='
260+
if ((sequenceReader.Length - consumedBytes) > (long)KeyLengthLimit + (long)ValueLengthLimit + 2)
259261
{
260262
ThrowKeyOrValueTooLargeException();
261263
}
@@ -319,17 +321,30 @@ private void ParseValuesSlow(
319321

320322
private void ThrowKeyOrValueTooLargeException()
321323
{
322-
throw new InvalidDataException($"Form key length limit {KeyLengthLimit} or value length limit {ValueLengthLimit} exceeded.");
324+
throw new InvalidDataException(
325+
string.Format(
326+
CultureInfo.CurrentCulture,
327+
Resources.FormPipeReader_KeyOrValueTooLarge,
328+
KeyLengthLimit,
329+
ValueLengthLimit));
323330
}
324331

325332
private void ThrowKeyTooLargeException()
326333
{
327-
throw new InvalidDataException($"Form key length limit {KeyLengthLimit} exceeded.");
334+
throw new InvalidDataException(
335+
string.Format(
336+
CultureInfo.CurrentCulture,
337+
Resources.FormPipeReader_KeyTooLarge,
338+
KeyLengthLimit));
328339
}
329340

330341
private void ThrowValueTooLargeException()
331342
{
332-
throw new InvalidDataException($"Form value length limit {ValueLengthLimit} exceeded.");
343+
throw new InvalidDataException(
344+
string.Format(
345+
CultureInfo.CurrentCulture,
346+
Resources.FormPipeReader_ValueTooLarge,
347+
ValueLengthLimit));
333348
}
334349

335350
[SkipLocalsInit]

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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Buffers;
66
using System.Collections.Generic;
7+
using System.Globalization;
78
using System.IO;
89
using System.IO.Pipelines;
910
using System.Text;
@@ -215,6 +216,34 @@ public async Task ReadFormAsync_ValueLengthLimitExceededAcrossBufferBoundary_Thr
215216
Assert.Equal(Encoding.UTF8.GetBytes("baz=12345678901"), readResult.Buffer.ToArray());
216217
}
217218

219+
[Fact]
220+
public void ReadFormAsync_ChunkedDataNoDelimiter_ThrowsEarly()
221+
{
222+
byte[] bytes = CreateBytes_NoDelimiter((10 * 1024) + 2);
223+
var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(bytes);
224+
225+
KeyValueAccumulator accumulator = default;
226+
227+
var valueLengthLimit = 1024;
228+
var keyLengthLimit = 10;
229+
230+
var formReader = new FormPipeReader(null!)
231+
{
232+
ValueLengthLimit = valueLengthLimit,
233+
KeyLengthLimit = keyLengthLimit
234+
};
235+
236+
var exception = Assert.Throws<InvalidDataException>(
237+
() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: false));
238+
// Make sure that FormPipeReader throws an exception after hitting KeyLengthLimit + ValueLengthLimit,
239+
// Rather than after reading the entire request.
240+
Assert.Equal(string.Format(
241+
CultureInfo.CurrentCulture,
242+
Resources.FormPipeReader_KeyOrValueTooLarge,
243+
keyLengthLimit,
244+
valueLengthLimit), exception.Message);
245+
}
246+
218247
// https://en.wikipedia.org/wiki/Percent-encoding
219248
[Theory]
220249
[InlineData("++=hello", " ", "hello")]
@@ -606,5 +635,17 @@ private static async Task<PipeReader> MakePipeReader(string text)
606635
bodyPipe.Writer.Complete();
607636
return bodyPipe.Reader;
608637
}
638+
639+
private static byte[] CreateBytes_NoDelimiter(int n)
640+
{
641+
//Create the bytes of "key=vvvvvvvv....", of length n
642+
var keyValue = new char[n];
643+
Array.Fill(keyValue, 'v');
644+
keyValue[0] = 'k';
645+
keyValue[1] = 'e';
646+
keyValue[2] = 'y';
647+
keyValue[3] = '=';
648+
return Encoding.UTF8.GetBytes(keyValue);
649+
}
609650
}
610651
}

0 commit comments

Comments
 (0)