Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit b6b7eb0

Browse files
authored
Implement MaxResponseHeadersLength on ManagedHandler (#26906)
1 parent 758b3f9 commit b6b7eb0

File tree

5 files changed

+233
-109
lines changed

5 files changed

+233
-109
lines changed

src/System.Net.Http/src/System/Net/Http/Managed/ChunkedEncodingReadStream.cs

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Buffers.Text;
56
using System.Diagnostics;
67
using System.IO;
78
using System.Threading;
@@ -13,81 +14,83 @@ internal sealed partial class HttpConnection
1314
{
1415
private sealed class ChunkedEncodingReadStream : HttpContentReadStream
1516
{
17+
/// <summary>How long a chunk indicator is allowed to be.</summary>
18+
/// <remarks>
19+
/// While most chunks indicators will contain no more than ulong.MaxValue.ToString("X").Length characters,
20+
/// "chunk extensions" are allowed. We place a limit on how long a line can be to avoid OOM issues if an
21+
/// infinite chunk length is sent. This value is arbitrary and can be changed as needed.
22+
/// </remarks>
23+
private const int MaxChunkBytesAllowed = 16*1024;
24+
/// <summary>How long a trailing header can be. This value is arbitrary and can be changed as needed.</summary>
25+
private const int MaxTrailingHeaderLength = 16*1024;
26+
/// <summary>The number of bytes remaining in the chunk.</summary>
1627
private ulong _chunkBytesRemaining;
1728

18-
public ChunkedEncodingReadStream(HttpConnection connection)
19-
: base(connection)
29+
public ChunkedEncodingReadStream(HttpConnection connection) : base(connection)
2030
{
21-
_chunkBytesRemaining = 0;
2231
}
2332

24-
private async Task<bool> TryGetNextChunk(CancellationToken cancellationToken)
33+
private async Task<bool> TryGetNextChunkAsync(CancellationToken cancellationToken)
2534
{
2635
Debug.Assert(_chunkBytesRemaining == 0);
2736

28-
// Start of chunk, read chunk size.
29-
ulong chunkSize = ParseHexSize(await _connection.ReadNextLineAsync(cancellationToken).ConfigureAwait(false));
30-
_chunkBytesRemaining = chunkSize;
31-
32-
if (chunkSize > 0)
33-
{
34-
return true;
35-
}
36-
37-
// We received a chunk size of 0, which indicates end of response body.
38-
// Read and discard any trailing headers, until we see an empty line.
39-
while (!LineIsEmpty(await _connection.ReadNextLineAsync(cancellationToken).ConfigureAwait(false)))
40-
;
37+
// Read the start of the chunk line.
38+
_connection._allowedReadLineBytes = MaxChunkBytesAllowed;
39+
ArraySegment<byte> line = await _connection.ReadNextLineAsync(cancellationToken).ConfigureAwait(false);
4140

42-
_connection.ReturnConnectionToPool();
43-
_connection = null;
44-
return false;
45-
}
46-
47-
private ulong ParseHexSize(ArraySegment<byte> line)
48-
{
49-
if (line.Count == 0)
41+
// Parse the hex value.
42+
if (!Utf8Parser.TryParse(line.AsReadOnlySpan(), out ulong chunkSize, out int bytesConsumed, 'X'))
5043
{
5144
throw new IOException(SR.net_http_invalid_response);
5245
}
53-
54-
ulong size = 0;
55-
try
46+
else if (bytesConsumed != line.Count)
5647
{
57-
for (int i = 0; i < line.Count; i++)
48+
// There was data after the chunk size, presumably a "chunk extension".
49+
// Allow tabs and spaces and then stop validating once we get to an extension.
50+
int offset = line.Offset + bytesConsumed, end = line.Count - bytesConsumed + line.Offset;
51+
for (int i = offset; i < end; i++)
5852
{
59-
char c = (char)line[i];
60-
if ((uint)(c - '0') <= '9' - '0')
61-
{
62-
size = checked(size * 16 + ((ulong)c - '0'));
63-
}
64-
else if ((uint)(c - 'a') <= ('f' - 'a'))
53+
char c = (char)line.Array[i];
54+
if (c == ';')
6555
{
66-
size = checked(size * 16 + ((ulong)c - 'a' + 10));
56+
break;
6757
}
68-
else if ((uint)(c - 'A') <= ('F' - 'A'))
69-
{
70-
size = checked(size * 16 + ((ulong)c - 'A' + 10));
71-
}
72-
else
58+
else if (c != ' ' && c != '\t') // not called out in the RFC, but WinHTTP allows it
7359
{
7460
throw new IOException(SR.net_http_invalid_response);
7561
}
7662
}
7763
}
78-
catch (OverflowException e)
64+
65+
_chunkBytesRemaining = chunkSize;
66+
if (chunkSize > 0)
7967
{
80-
throw new IOException(SR.net_http_invalid_response, e);
68+
return true;
8169
}
82-
return size;
70+
71+
// We received a chunk size of 0, which indicates end of response body.
72+
// Read and discard any trailing headers, until we see an empty line.
73+
while (true)
74+
{
75+
_connection._allowedReadLineBytes = MaxTrailingHeaderLength;
76+
if (LineIsEmpty(await _connection.ReadNextLineAsync(cancellationToken).ConfigureAwait(false)))
77+
{
78+
break;
79+
}
80+
}
81+
82+
_connection.ReturnConnectionToPool();
83+
_connection = null;
84+
return false;
8385
}
8486

85-
private async Task ConsumeChunkBytes(ulong bytesConsumed, CancellationToken cancellationToken)
87+
private async Task ConsumeChunkBytesAsync(ulong bytesConsumed, CancellationToken cancellationToken)
8688
{
8789
Debug.Assert(bytesConsumed <= _chunkBytesRemaining);
8890
_chunkBytesRemaining -= bytesConsumed;
8991
if (_chunkBytesRemaining == 0)
9092
{
93+
_connection._allowedReadLineBytes = 2; // \r\n
9194
if (!LineIsEmpty(await _connection.ReadNextLineAsync(cancellationToken).ConfigureAwait(false)))
9295
{
9396
ThrowInvalidHttpResponse();
@@ -111,7 +114,7 @@ public override async ValueTask<int> ReadAsync(Memory<byte> destination, Cancell
111114

112115
if (_chunkBytesRemaining == 0)
113116
{
114-
if (!await TryGetNextChunk(cancellationToken).ConfigureAwait(false))
117+
if (!await TryGetNextChunkAsync(cancellationToken).ConfigureAwait(false))
115118
{
116119
// End of response body
117120
return 0;
@@ -131,7 +134,7 @@ public override async ValueTask<int> ReadAsync(Memory<byte> destination, Cancell
131134
throw new IOException(SR.net_http_invalid_response);
132135
}
133136

134-
await ConsumeChunkBytes((ulong)bytesRead, cancellationToken).ConfigureAwait(false);
137+
await ConsumeChunkBytesAsync((ulong)bytesRead, cancellationToken).ConfigureAwait(false);
135138

136139
return bytesRead;
137140
}
@@ -152,13 +155,13 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance
152155
if (_chunkBytesRemaining > 0)
153156
{
154157
await _connection.CopyToAsync(destination, _chunkBytesRemaining, cancellationToken).ConfigureAwait(false);
155-
await ConsumeChunkBytes(_chunkBytesRemaining, cancellationToken).ConfigureAwait(false);
158+
await ConsumeChunkBytesAsync(_chunkBytesRemaining, cancellationToken).ConfigureAwait(false);
156159
}
157160

158-
while (await TryGetNextChunk(cancellationToken).ConfigureAwait(false))
161+
while (await TryGetNextChunkAsync(cancellationToken).ConfigureAwait(false))
159162
{
160163
await _connection.CopyToAsync(destination, _chunkBytesRemaining, cancellationToken).ConfigureAwait(false);
161-
await ConsumeChunkBytes(_chunkBytesRemaining, cancellationToken).ConfigureAwait(false);
164+
await ConsumeChunkBytesAsync(_chunkBytesRemaining, cancellationToken).ConfigureAwait(false);
162165
}
163166
}
164167
}

src/System.Net.Http/src/System/Net/Http/Managed/HttpConnection.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ internal sealed partial class HttpConnection : IDisposable
5656
private readonly byte[] _writeBuffer;
5757
private int _writeOffset;
5858
private Exception _pendingException;
59+
private int _allowedReadLineBytes;
5960

6061
private Task<int> _readAheadTask;
6162
private byte[] _readBuffer;
@@ -226,7 +227,7 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
226227

227228
await WriteStringAsync(request.RequestUri.PathAndQuery, cancellationToken).ConfigureAwait(false);
228229

229-
// fall-back to 1.1 for all versions other than 1.0
230+
// Fall back to 1.1 for all versions other than 1.0
230231
Debug.Assert(request.Version.Major >= 0 && request.Version.Minor >= 0); // guaranteed by Version class
231232
bool isHttp10 = request.Version.Minor == 0 && request.Version.Major == 1;
232233
await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11NewlineAsciiBytes,
@@ -313,6 +314,7 @@ await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11N
313314
}
314315

315316
// Start to read response.
317+
_allowedReadLineBytes = _pool.Pools.Settings._maxResponseHeadersLength * 1024;
316318

317319
// We should not have any buffered data here; if there was, it should have been treated as an error
318320
// by the previous request handling. (Note we do not support HTTP pipelining.)
@@ -378,6 +380,7 @@ await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11N
378380
{
379381
ThrowInvalidHttpResponse();
380382
}
383+
381384
ParseStatusLine(await ReadNextLineAsync(cancellationToken).ConfigureAwait(false), response);
382385
}
383386
}
@@ -474,10 +477,7 @@ await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11N
474477
}
475478
}
476479

477-
private static bool LineIsEmpty(ArraySegment<byte> line)
478-
{
479-
return line.Count == 0;
480-
}
480+
private static bool LineIsEmpty(ArraySegment<byte> line) => line.Count == 0;
481481

482482
private async Task SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream)
483483
{
@@ -896,6 +896,11 @@ private async ValueTask<ArraySegment<byte>> ReadNextLineAsync(CancellationToken
896896
}
897897

898898
// Advance read position past the LF
899+
_allowedReadLineBytes -= lfIndex + 1 - scanOffset;
900+
if (_allowedReadLineBytes < 0)
901+
{
902+
ThrowInvalidHttpResponse();
903+
}
899904
_readOffset = lfIndex + 1;
900905

901906
return new ArraySegment<byte>(_readBuffer, startIndex, length);
@@ -904,6 +909,11 @@ private async ValueTask<ArraySegment<byte>> ReadNextLineAsync(CancellationToken
904909
// Couldn't find LF. Read more.
905910
// Note this may cause _readOffset to change.
906911
previouslyScannedBytes = _readLength - _readOffset;
912+
_allowedReadLineBytes -= _readLength - scanOffset;
913+
if (_allowedReadLineBytes < 0)
914+
{
915+
ThrowInvalidHttpResponse();
916+
}
907917
await FillAsync(cancellationToken).ConfigureAwait(false);
908918
}
909919
}

0 commit comments

Comments
 (0)