Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 2 deletions eng/Baseline.Designer.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<AspNetCoreBaselineVersion>2.3.5</AspNetCoreBaselineVersion>
<AspNetCoreBaselineVersion>2.3.6</AspNetCoreBaselineVersion>
</PropertyGroup>
<!-- Package: dotnet-dev-certs-->
<PropertyGroup Condition=" '$(PackageId)' == 'dotnet-dev-certs' ">
Expand Down Expand Up @@ -889,7 +889,7 @@
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.Server.Kestrel.Core-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Server.Kestrel.Core' ">
<BaselinePackageVersion>2.3.0</BaselinePackageVersion>
<BaselinePackageVersion>2.3.6</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Server.Kestrel.Core' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="[2.3.0, )" />
Expand Down
4 changes: 2 additions & 2 deletions eng/Baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file contains a list of all the packages and their versions which were rele
build of ASP.NET Core 2.1.x. Update this list when preparing for a new patch.

-->
<Baseline Version="2.3.5">
<Baseline Version="2.3.6">
<Package Id="dotnet-dev-certs" Version="2.1.1" />
<Package Id="dotnet-sql-cache" Version="2.1.1" />
<Package Id="dotnet-user-secrets" Version="2.1.1" />
Expand Down Expand Up @@ -99,7 +99,7 @@ build of ASP.NET Core 2.1.x. Update this list when preparing for a new patch.
<Package Id="Microsoft.AspNetCore.Routing" Version="2.3.0" />
<Package Id="Microsoft.AspNetCore.Server.HttpSys" Version="2.3.0" />
<Package Id="Microsoft.AspNetCore.Server.IISIntegration" Version="2.3.0" />
<Package Id="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.0" />
<Package Id="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.6" />
<Package Id="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.3.0" />
<Package Id="Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions" Version="2.3.0" />
<Package Id="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" Version="2.3.0" />
Expand Down
5 changes: 5 additions & 0 deletions eng/PatchConfig.props
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ Later on, this will be checked using this condition:
</PackagesInPatch>
</PropertyGroup>
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.3.6' ">
<PackagesInPatch>
Microsoft.AspNetCore.Server.Kestrel.Core;
</PackagesInPatch>
</PropertyGroup>
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.3.7' ">
<PackagesInPatch>
</PackagesInPatch>
</PropertyGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/Servers/Kestrel/Core/src/BadHttpRequestException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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
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 @@ -518,4 +518,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="BadDeveloperCertificateState" xml:space="preserve">
<value>The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.</value>
</data>
<data name="BadRequest_BadChunkExtension" xml:space="preserve">
<value>Bad chunk extension.</value>
</data>
</root>
60 changes: 46 additions & 14 deletions src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ private class ForChunkedEncoding : 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 @@ -401,6 +402,13 @@ private class ForChunkedEncoding : Http1MessageBody

private Mode _mode = Mode.Prefix;

private static readonly bool InsecureChunkedParsing;

static ForChunkedEncoding()
{
InsecureChunkedParsing = AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.Kestrel.EnableInsecureChunkedRequestParsing", out var value) && value;
}

public ForChunkedEncoding(bool keepAlive, Http1Connection context)
: base(context)
{
Expand Down Expand Up @@ -554,56 +562,80 @@ private void ParseChunkedPrefix(ReadOnlySequence<byte> buffer, out SequencePosit
BadHttpRequestException.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
consumed = buffer.Start;
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;
AddAndCheckConsumedBytes(buffer.Length);
return;
};
}

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

var sufixBuffer = buffer.Slice(extensionCursor);
if (sufixBuffer.Length < 2)
var suffixBuffer = buffer.Slice(extensionCursor);
if (suffixBuffer.Length < 2)
{
consumed = extensionCursor;
examined = buffer.End;
AddAndCheckConsumedBytes(charsToByteCRExclusive);
return;
}

sufixBuffer = sufixBuffer.Slice(0, 2);
var sufixSpan = sufixBuffer.ToSpan();
suffixBuffer = suffixBuffer.Slice(0, 2);
var suffixSpan = suffixBuffer.ToSpan();

if (sufixSpan[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;

consumed = sufixBuffer.End;
examined = sufixBuffer.End;
consumed = suffixBuffer.End;
examined = suffixBuffer.End;
AddAndCheckConsumedBytes(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;
AddAndCheckConsumedBytes(charsToByteCRExclusive + 1);
}
else
{
consumed = suffixBuffer.End;
examined = suffixBuffer.End;
// We have \rX or \nX, that's an invalid extension.
BadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension);
}
} while (_mode == Mode.Extension);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private static unsafe void EncodeAsciiCharsToBytes(char* input, byte* output, in
i += 4;
}

trailing:
trailing:
for (; i < length; i++)
{
char ch = *(input + i);
Expand Down Expand Up @@ -269,5 +269,49 @@ private static byte[] CreateNumericBytesScratch()
_numericBytesScratch = bytes;
return bytes;
}

/// <summary>
/// Returns position of first occurrence of item in the <see cref="ReadOnlySequence{T}"/>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SequencePosition? PositionOfAny<T>(in this ReadOnlySequence<T> source, T value0, T value1) where T : IEquatable<T>
{
if (source.IsSingleSegment)
{
int index = source.First.Span.IndexOfAny(value0, value1);
if (index != -1)
{
return source.GetPosition(index);
}

return null;
}
else
{
return PositionOfAnyMultiSegment(source, value0, value1);
}
}

private static SequencePosition? PositionOfAnyMultiSegment<T>(in ReadOnlySequence<T> source, T value0, T value1) where T : IEquatable<T>
{
SequencePosition position = source.Start;
SequencePosition result = position;
while (source.TryGet(ref position, out ReadOnlyMemory<T> memory))
{
int index = memory.Span.IndexOfAny(value0, value1);
if (index != -1)
{
return source.GetPosition(index, result);
}
else if (position.GetObject() == null)
{
break;
}

result = position;
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum RequestRejectionReason
UnexpectedEndOfRequestContent,
BadChunkSuffix,
BadChunkSizeData,
BadChunkExtension,
ChunkedRequestIncomplete,
InvalidRequestTarget,
InvalidCharactersInHeaderName,
Expand All @@ -32,6 +33,6 @@ public enum RequestRejectionReason
MissingHostHeader,
MultipleHostHeaders,
InvalidHostHeader,
RequestBodyExceedsContentLength
RequestBodyExceedsContentLength,
}
}
28 changes: 14 additions & 14 deletions src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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 @@ -139,14 +139,14 @@ public async Task ReadExitsGivenIncompleteChunkedExtension()
var stream = new HttpRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.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());
Assert.Equal(0, await stream.ReadAsync(buffer, 0, buffer.Length));
Expand Down
Loading
Loading