Skip to content

Commit 089c3d5

Browse files
Apply HeadersLengthLimit to preamble in MultipartReader (#59646)
1 parent f7ff82f commit 089c3d5

File tree

4 files changed

+63
-9
lines changed

4 files changed

+63
-9
lines changed

src/Http/WebUtilities/src/MultipartBoundary.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ internal sealed class MultipartBoundary
1010
private readonly byte[] _boundaryBytes;
1111
private bool _expectLeadingCrlf;
1212

13-
public MultipartBoundary(string boundary, bool expectLeadingCrlf = true)
13+
public MultipartBoundary(string boundary)
1414
{
1515
ArgumentNullException.ThrowIfNull(boundary);
1616

17-
_expectLeadingCrlf = expectLeadingCrlf;
17+
_expectLeadingCrlf = false;
1818
_boundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary);
1919

2020
FinalBoundaryLength = BoundaryBytes.Length + 2; // Include the final '--' terminator.
@@ -25,6 +25,12 @@ public void ExpectLeadingCrlf()
2525
_expectLeadingCrlf = true;
2626
}
2727

28+
// Lets us throw a more specific error from MultipartReaderStream when reading any preamble data.
29+
public bool BeforeFirstBoundary()
30+
{
31+
return !_expectLeadingCrlf;
32+
}
33+
2834
// Return either "--{boundary}" or "\r\n--{boundary}" depending on if we're looking for the end of a section
2935
public ReadOnlySpan<byte> BoundaryBytes => _boundaryBytes.AsSpan(_expectLeadingCrlf ? 0 : 2);
3036

src/Http/WebUtilities/src/MultipartReader.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class MultipartReader
2727

2828
private readonly BufferedReadStream _stream;
2929
private readonly MultipartBoundary _boundary;
30-
private MultipartReaderStream _currentStream;
30+
private MultipartReaderStream? _currentStream;
3131

3232
/// <summary>
3333
/// Initializes a new instance of <see cref="MultipartReader"/>.
@@ -56,10 +56,7 @@ public MultipartReader(string boundary, Stream stream, int bufferSize)
5656
}
5757
_stream = new BufferedReadStream(stream, bufferSize);
5858
boundary = HeaderUtilities.RemoveQuotes(new StringSegment(boundary)).ToString();
59-
_boundary = new MultipartBoundary(boundary, false);
60-
// This stream will drain any preamble data and remove the first boundary marker.
61-
// TODO: HeadersLengthLimit can't be modified until after the constructor.
62-
_currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit };
59+
_boundary = new MultipartBoundary(boundary);
6360
}
6461

6562
/// <summary>
@@ -86,6 +83,10 @@ public MultipartReader(string boundary, Stream stream, int bufferSize)
8683
/// <returns></returns>
8784
public async Task<MultipartSection?> ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken())
8885
{
86+
// Only occurs on first call
87+
// This stream will drain any preamble data and remove the first boundary marker.
88+
_currentStream ??= new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit };
89+
8990
// Drain the prior section.
9091
await _currentStream.DrainAsync(cancellationToken);
9192
// If we're at the end return null

src/Http/WebUtilities/src/MultipartReaderStream.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,19 @@ private int UpdatePosition(int read)
145145
if (_observedLength < _position)
146146
{
147147
_observedLength = _position;
148-
if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault())
148+
if (LengthLimit.HasValue &&
149+
LengthLimit.GetValueOrDefault() is var lengthLimit &&
150+
_observedLength > lengthLimit)
149151
{
150-
throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.");
152+
// If we hit the limit before the first boundary then we're using the header length limit, not the body length limit.
153+
if (_boundary.BeforeFirstBoundary())
154+
{
155+
throw new InvalidDataException($"Multipart header length limit {lengthLimit} exceeded. Too much data before the first boundary.");
156+
}
157+
else
158+
{
159+
throw new InvalidDataException($"Multipart body length limit {lengthLimit} exceeded.");
160+
}
151161
}
152162
}
153163
return read;

src/Http/WebUtilities/test/MultipartReaderTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,43 @@ public async Task MultipartReader_HeadersLengthExceeded_Throws()
147147
Assert.Equal("Line length limit 17 exceeded.", exception.Message);
148148
}
149149

150+
[Fact]
151+
public async Task MultipartReader_HeadersLengthExceeded_LargePreamble()
152+
{
153+
var body = $"preamble {new string('a', 17000)}\r\n" +
154+
"--9051914041544843365972754266\r\n" +
155+
"\r\n" +
156+
"text default\r\n" +
157+
"--9051914041544843365972754266--\r\n";
158+
var stream = MakeStream(body);
159+
var reader = new MultipartReader(Boundary, stream);
160+
161+
var exception = await Assert.ThrowsAsync<InvalidDataException>(() => reader.ReadNextSectionAsync());
162+
Assert.Equal("Multipart header length limit 16384 exceeded. Too much data before the first boundary.", exception.Message);
163+
}
164+
165+
[Fact]
166+
public async Task MultipartReader_HeadersLengthLimitSettable_LargePreamblePasses()
167+
{
168+
var body = $"preamble {new string('a', 100_000)}\r\n" +
169+
"--9051914041544843365972754266\r\n" +
170+
"\r\n" +
171+
"text default\r\n" +
172+
"--9051914041544843365972754266--\r\n";
173+
var stream = MakeStream(body);
174+
var reader = new MultipartReader(Boundary, stream)
175+
{
176+
HeadersLengthLimit = 200_000,
177+
};
178+
179+
var section = await reader.ReadNextSectionAsync();
180+
Assert.NotNull(section);
181+
182+
var buffer = new MemoryStream();
183+
await section.Body.CopyToAsync(buffer);
184+
Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray()));
185+
}
186+
150187
[Fact]
151188
public async Task MultipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success()
152189
{

0 commit comments

Comments
 (0)