Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4255dda
Merge commit '96103c4a586e28b7ddf7629e1a7d5e79a097d285'
Sep 3, 2025
22d2155
Merge commit 'aea22332d6908d84626ffa94c4fcbfbde8203c5a'
Sep 3, 2025
3321e2f
Merge commit 'd168ab01a3efb15dd9a43aa35e134413f5560bb3'
Sep 4, 2025
db580fd
Merge commit '773460572b6ffac6c5e4b7c1c9afbcb5a7e9353e'
Sep 4, 2025
b3e8ef0
Merge commit '5f58326b6a4cfb09c94000375b956d0ec02322b5'
Sep 4, 2025
c2bebc9
Merge commit '81b394c4271099e3cbb2baeeaf9d7c7f889de160'
Sep 5, 2025
73be5c6
Merge commit '63b74906940af632db30a41fe87b95d5cb0e9ec3'
Sep 8, 2025
97a8643
Merged PR 51302: Fix chunked request parsing
BrennanConroy Sep 9, 2025
b795a4f
Merge commit '78e1dd9e2616d8e3b686e9790f439043f95d5ab6'
Sep 9, 2025
57e0bf8
Merge commit 'd7cc8995873dbe7c222b54e020862d49f923dc37'
Sep 13, 2025
6875c03
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 16, 2025
d8ad1d0
Merged PR 53383: [internal/release/9.0] Update dependencies from dnce…
Sep 16, 2025
0a6a48d
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 17, 2025
a2c5549
Merged PR 53401: [internal/release/9.0] Update dependencies from dnce…
Sep 17, 2025
940db9c
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 17, 2025
4cff595
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 17, 2025
cf00784
Merged PR 53431: [internal/release/9.0] Update dependencies from dnce…
Sep 17, 2025
e0d667c
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 17, 2025
f2e8b14
Merged PR 53443: [internal/release/9.0] Update dependencies from dnce…
Sep 18, 2025
a5dc8fa
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 19, 2025
ddb7987
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 19, 2025
5a264b2
Merged PR 53493: [internal/release/9.0] Update dependencies from dnce…
Sep 19, 2025
18d3a4b
Merge commit 'e820f5876944f21a3cd87cf12093cbf48ed8a90c'
Sep 19, 2025
414e63b
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 22, 2025
15ed1d3
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 22, 2025
0e2f248
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 23, 2025
fa3b744
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 23, 2025
0994268
Merged PR 53574: [internal/release/9.0] Update dependencies from dnce…
Sep 23, 2025
ead83a1
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 23, 2025
7c94b04
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 24, 2025
ad9875e
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Sep 25, 2025
4bfc8d0
Merged PR 53656: [internal/release/9.0] Update dependencies from dnce…
Sep 25, 2025
5ead537
Merged PR 53785: [internal/release/9.0] Update dependencies from dnce…
Sep 26, 2025
5bae930
Merged PR 53806: [internal/release/9.0] Update dependencies from dnce…
Sep 26, 2025
bd93ac9
Merge commit '5bae930797f60d2d04f3b1df6a33eaca85fc5f28' into internal…
vseanreesermsft Oct 14, 2025
7e86abb
Update baseline, SDK
wtgodbe Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
<clear />
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
<!-- Begin: Package sources from dotnet-runtime -->
<add key="darc-int-dotnet-runtime-e1f1988" value="https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-int-dotnet-runtime-e1f19886/nuget/v3/index.json" />
<!-- End: Package sources from dotnet-runtime -->
<!-- Begin: Package sources from dotnet-efcore -->
<add key="darc-int-dotnet-efcore-5452ff9" value="https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-int-dotnet-efcore-5452ff90/nuget/v3/index.json" />
<!-- End: Package sources from dotnet-efcore -->
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
Expand All @@ -28,8 +30,10 @@
<clear />
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
<!-- Begin: Package sources from dotnet-efcore -->
<add key="darc-int-dotnet-efcore-5452ff9" value="true" />
<!-- End: Package sources from dotnet-efcore -->
<!-- Begin: Package sources from dotnet-runtime -->
<add key="darc-int-dotnet-runtime-e1f1988" value="true" />
<!-- End: Package sources from dotnet-runtime -->
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
</disabledPackageSources>
Expand Down
776 changes: 388 additions & 388 deletions eng/Baseline.Designer.props

Large diffs are not rendered by default.

212 changes: 106 additions & 106 deletions eng/Baseline.xml

Large diffs are not rendered by default.

320 changes: 160 additions & 160 deletions eng/Version.Details.xml

Large diffs are not rendered by default.

162 changes: 81 additions & 81 deletions eng/Versions.props

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"sdk": {
"version": "9.0.110"
"version": "9.0.111"
},
"tools": {
"dotnet": "9.0.110",
"dotnet": "9.0.111",
"runtimes": {
"dotnet/x86": [
"$(MicrosoftNETCoreBrowserDebugHostTransportVersion)"
Expand Down
3 changes: 3 additions & 0 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -740,4 +740,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http3ControlStreamFrameTooLarge" xml:space="preserve">
<value>The client sent a {frameType} frame to a control stream that was too large.</value>
</data>
<data name="BadRequest_BadChunkExtension" xml:space="preserve">
<value>Bad chunk extension.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal sealed class Http1ChunkedEncodingMessageBody : Http1MessageBody
{
// byte consts don't have a data type annotation so we pre-cast it
private const byte ByteCR = (byte)'\r';
private const byte ByteLF = (byte)'\n';
// "7FFFFFFF\r\n" is the largest chunk size that could be returned as an int.
private const int MaxChunkPrefixBytes = 10;

Expand All @@ -27,6 +28,8 @@ internal sealed class Http1ChunkedEncodingMessageBody : Http1MessageBody
private readonly Pipe _requestBodyPipe;
private ReadResult _readResult;

Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This AppContext switch should be documented with XML comments explaining its purpose, security implications, and recommended usage scenarios.

Suggested change
/// <summary>
/// Indicates whether insecure chunked request parsing is enabled via the AppContext switch
/// "Microsoft.AspNetCore.Server.Kestrel.EnableInsecureChunkedRequestParsing".
/// <para>
/// <b>Purpose:</b> Allows legacy clients to send chunked requests that do not strictly conform to RFC 2616.
/// </para>
/// <para>
/// <b>Security Implications:</b> Enabling this switch may expose the server to request smuggling or other
/// parsing vulnerabilities. It should only be enabled if absolutely necessary and with a full understanding
/// of the risks.
/// </para>
/// <para>
/// <b>Recommended Usage:</b> This switch should remain disabled in production environments. Only enable for
/// legacy compatibility scenarios where strict parsing breaks interoperability, and after a thorough security review.
/// </para>
/// </summary>

Copilot uses AI. Check for mistakes.

private static readonly bool InsecureChunkedParsing = AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.Kestrel.EnableInsecureChunkedRequestParsing", out var value) && value;

public Http1ChunkedEncodingMessageBody(Http1Connection context, bool keepAlive)
: base(context, keepAlive)
{
Expand Down Expand Up @@ -345,25 +348,42 @@ private void ParseChunkedPrefix(in ReadOnlySequence<byte> buffer, out SequencePo
KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkSizeData);
}

// https://www.rfc-editor.org/rfc/rfc9112#section-7.1
// chunk = chunk-size [ chunk-ext ] CRLF
// chunk-data CRLF

// https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1
// chunk-ext = *( BWS ";" BWS chunk-ext-name
// [BWS "=" BWS chunk-ext-val] )
// chunk-ext-name = token
// chunk-ext-val = token / quoted-string
private void ParseExtension(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
{
// Chunk-extensions not currently parsed
// Just drain the data
examined = buffer.Start;
// Chunk-extensions parsed for \r\n and throws for unpaired \r or \n.

do
{
SequencePosition? extensionCursorPosition = buffer.PositionOf(ByteCR);
SequencePosition? extensionCursorPosition;
if (InsecureChunkedParsing)
{
extensionCursorPosition = buffer.PositionOf(ByteCR);
}
else
{
extensionCursorPosition = buffer.PositionOfAny(ByteCR, ByteLF);
}

if (extensionCursorPosition == null)
{
// End marker not found yet
consumed = buffer.End;
examined = buffer.End;
AddAndCheckObservedBytes(buffer.Length);
return;
};
}

var extensionCursor = extensionCursorPosition.Value;

var charsToByteCRExclusive = buffer.Slice(0, extensionCursor).Length;

var suffixBuffer = buffer.Slice(extensionCursor);
Expand All @@ -378,7 +398,9 @@ private void ParseExtension(ReadOnlySequence<byte> buffer, out SequencePosition
suffixBuffer = suffixBuffer.Slice(0, 2);
var suffixSpan = suffixBuffer.ToSpan();

if (suffixSpan[1] == '\n')
if (InsecureChunkedParsing
? (suffixSpan[1] == ByteLF)
: (suffixSpan[0] == ByteCR && suffixSpan[1] == ByteLF))
{
// We consumed the \r\n at the end of the extension, so switch modes.
_mode = _inputLength > 0 ? Mode.Data : Mode.Trailer;
Expand All @@ -387,13 +409,22 @@ private void ParseExtension(ReadOnlySequence<byte> buffer, out SequencePosition
examined = suffixBuffer.End;
AddAndCheckObservedBytes(charsToByteCRExclusive + 2);
}
else
else if (InsecureChunkedParsing)
{
examined = buffer.Start;
// Don't consume suffixSpan[1] in case it is also a \r.
buffer = buffer.Slice(charsToByteCRExclusive + 1);
consumed = extensionCursor;
AddAndCheckObservedBytes(charsToByteCRExclusive + 1);
}
else
{
consumed = suffixBuffer.End;
examined = suffixBuffer.End;

// We have \rX or \nX, that's an invalid extension.
KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension);
}
} while (_mode == Mode.Extension);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal enum RequestRejectionReason
UnexpectedEndOfRequestContent,
BadChunkSuffix,
BadChunkSizeData,
BadChunkExtension,
ChunkedRequestIncomplete,
InvalidRequestTarget,
InvalidCharactersInHeaderName,
Expand All @@ -31,5 +32,5 @@ internal enum RequestRejectionReason
ConnectMethodRequired,
MissingHostHeader,
MultipleHostHeaders,
InvalidHostHeader
InvalidHostHeader,
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ internal static BadHttpRequestException GetException(RequestRejectionReason reas
case RequestRejectionReason.BadChunkSizeData:
ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkSizeData, StatusCodes.Status400BadRequest, reason);
break;
case RequestRejectionReason.BadChunkExtension:
ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkExtension, StatusCodes.Status400BadRequest, reason);
break;
case RequestRejectionReason.ChunkedRequestIncomplete:
ex = new BadHttpRequestException(CoreStrings.BadRequest_ChunkedRequestIncomplete, StatusCodes.Status400BadRequest, reason);
break;
Expand Down
4 changes: 2 additions & 2 deletions src/Servers/Kestrel/Core/test/MessageBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,14 +338,14 @@ public async Task ReadExitsGivenIncompleteChunkedExtension()
var stream = new HttpRequestStream(Mock.Of<IHttpBodyControlFeature>(), reader);
reader.StartAcceptingReads(body);

input.Add("5;\r\0");
input.Add("5;\r");

var buffer = new byte[1024];
var readTask = stream.ReadAsync(buffer, 0, buffer.Length);

Assert.False(readTask.IsCompleted);

input.Add("\r\r\r\nHello\r\n0\r\n\r\n");
input.Add("\nHello\r\n0\r\n\r\n");

Assert.Equal(5, await readTask.DefaultTimeout());
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Buffers;
using System.Globalization;
using System.Text;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
Expand All @@ -18,6 +19,70 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;

public class ChunkedRequestTests : LoggedTest
{
[Theory]
[InlineData("2;\rxx\r\nxy\r\n0")] // \r in chunk extensions
[InlineData("2;\nxx\r\nxy\r\n0")] // \n in chunk extensions
public async Task RejectsInvalidChunkExtensions(string invalidChunkLine)
{
var testContext = new TestServiceContext(LoggerFactory);

await using (var server = new TestServer(AppChunked, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"POST / HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"Content-Type: text/plain",
"",
invalidChunkLine,
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 400 Bad Request",
"Content-Length: 0",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"",
"");
}
}
}

[Theory]
[InlineData("2;a=b;b=c\r\nxy\r\n0")] // Multiple chunk extensions
[InlineData("2; \r\nxy\r\n0")] // Space in chunk extensions (BWS)
[InlineData("2;;;\r\nxy\r\n0")] // Multiple ';' in chunk extensions
[InlineData("2;novalue\r\nxy\r\n0")] // Name only chunk extension
//[InlineData("2 ;\r\nxy\r\n0")] // Technically allowed per spec, but we never supported it, and no one should be sending it
public async Task AllowsValidChunkExtensions(string chunkLine)
{
var testContext = new TestServiceContext(LoggerFactory);

await using (var server = new TestServer(AppChunked, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"POST / HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"Content-Type: text/plain",
"",
chunkLine,
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
"Content-Length: 2",
$"Date: {testContext.DateHeaderValue}",
"",
"xy");
}
}
}

private async Task App(HttpContext httpContext)
{
var request = httpContext.Request;
Expand Down Expand Up @@ -1117,4 +1182,86 @@ await connection.Receive(
}
}
}

[Fact]
public async Task MultiReadWithInvalidNewlineAcrossReads()
{
// Inline so that we know when the first connection.Send has been parsed so we can send the next part
var testContext = new TestServiceContext(LoggerFactory)
{ Scheduler = System.IO.Pipelines.PipeScheduler.Inline };

var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

await using (var server = new TestServer(async httpContext =>
{
var request = httpContext.Request;
var readTask = request.BodyReader.ReadAsync();
tcs.TrySetResult();
var readResult = await readTask;
request.BodyReader.AdvanceTo(readResult.Buffer.End);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.SendAll(
"GET / HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"",
"1;\r");
await tcs.Task;
await connection.SendAll(
"\r");

await connection.ReceiveEnd(
"HTTP/1.1 400 Bad Request",
"Content-Length: 0",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"",
"");
}
}
}

[Fact]
public async Task InvalidNewlineInFirstReadWithPartialChunkExtension()
{
// Inline so that we know when the first connection.Send has been parsed so we can send the next part
var testContext = new TestServiceContext(LoggerFactory)
{ Scheduler = System.IO.Pipelines.PipeScheduler.Inline };

var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

await using (var server = new TestServer(async httpContext =>
{
var request = httpContext.Request;
var readTask = request.BodyReader.ReadAsync();
tcs.TrySetResult();
var readResult = await readTask;
request.BodyReader.AdvanceTo(readResult.Buffer.End);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.SendAll(
"GET / HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"",
"1;\n");
await tcs.Task;
await connection.SendAll(
"t");

await connection.ReceiveEnd(
"HTTP/1.1 400 Bad Request",
"Content-Length: 0",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"",
"");
}
}
}
}
Loading