Make connection options parsing "safe" (#21004)

This commit is contained in:
Ben Adams 2020-04-30 00:23:56 +01:00 committed by GitHub
parent 9b44d28df7
commit 3af92e29b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 75 deletions

View File

@ -124,8 +124,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection);
upgrade = (connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade;
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
upgrade = (connectionOptions & ConnectionOptions.Upgrade) != 0;
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) != 0;
}
if (upgrade)

View File

@ -8,11 +8,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
@ -277,101 +275,146 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
public static unsafe ConnectionOptions ParseConnection(StringValues connection)
public static ConnectionOptions ParseConnection(StringValues connection)
{
// Keep-alive
const ulong lowerCaseKeep = 0x0000_0020_0020_0020; // Don't lowercase hyphen
const ulong keepAliveStart = 0x002d_0070_0065_0065; // 4 chars "eep-"
const ulong keepAliveMiddle = 0x0076_0069_006c_0061; // 4 chars "aliv"
const ushort keepAliveEnd = 0x0065; // 1 char "e"
// Upgrade
const ulong upgradeStart = 0x0061_0072_0067_0070; // 4 chars "pgra"
const uint upgradeEnd = 0x0065_0064; // 2 chars "de"
// Close
const ulong closeEnd = 0x0065_0073_006f_006c; // 4 chars "lose"
var connectionOptions = ConnectionOptions.None;
var connectionCount = connection.Count;
for (var i = 0; i < connectionCount; i++)
{
var value = connection[i];
fixed (char* ptr = value)
var value = connection[i].AsSpan();
while (value.Length > 0)
{
var ch = ptr;
var tokenEnd = ch;
var end = ch + value.Length;
while (ch < end)
int offset;
char c = '\0';
// Skip any spaces and empty values.
for (offset = 0; offset < value.Length; offset++)
{
while (tokenEnd < end && *tokenEnd != ',')
c = value[offset];
if (c != ' ' && c != ',')
{
tokenEnd++;
break;
}
}
while (ch < tokenEnd && *ch == ' ')
// Skip last read char.
offset++;
if ((uint)offset > (uint)value.Length)
{
// Consumed enitre string, move to next.
break;
}
// Remove leading spaces or empty values.
value = value.Slice(offset);
c = ToLowerCase(c);
var byteValue = MemoryMarshal.AsBytes(value);
offset = 0;
var potentialConnectionOptions = ConnectionOptions.None;
if (c == 'k' && byteValue.Length >= (2 * sizeof(ulong) + sizeof(ushort)))
{
if ((BinaryPrimitives.ReadUInt64LittleEndian(byteValue) | lowerCaseKeep) == keepAliveStart)
{
ch++;
}
offset += sizeof(ulong) / 2;
byteValue = byteValue.Slice(sizeof(ulong));
var tokenLength = tokenEnd - ch;
if (tokenLength >= 9 && (*ch | 0x20) == 'k')
{
if ((*++ch | 0x20) == 'e' &&
(*++ch | 0x20) == 'e' &&
(*++ch | 0x20) == 'p' &&
*++ch == '-' &&
(*++ch | 0x20) == 'a' &&
(*++ch | 0x20) == 'l' &&
(*++ch | 0x20) == 'i' &&
(*++ch | 0x20) == 'v' &&
(*++ch | 0x20) == 'e')
if (ReadLowerCaseUInt64(byteValue) == keepAliveMiddle)
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
offset += sizeof(ulong) / 2;
byteValue = byteValue.Slice(sizeof(ulong));
if (ch == tokenEnd)
if (ReadLowerCaseUInt16(byteValue) == keepAliveEnd)
{
connectionOptions |= ConnectionOptions.KeepAlive;
offset += sizeof(ushort) / 2;
potentialConnectionOptions = ConnectionOptions.KeepAlive;
}
}
}
else if (tokenLength >= 7 && (*ch | 0x20) == 'u')
}
else if (c == 'u' && byteValue.Length >= (sizeof(ulong) + sizeof(uint)))
{
if (ReadLowerCaseUInt64(byteValue) == upgradeStart)
{
if ((*++ch | 0x20) == 'p' &&
(*++ch | 0x20) == 'g' &&
(*++ch | 0x20) == 'r' &&
(*++ch | 0x20) == 'a' &&
(*++ch | 0x20) == 'd' &&
(*++ch | 0x20) == 'e')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
offset += sizeof(ulong) / 2;
byteValue = byteValue.Slice(sizeof(ulong));
if (ch == tokenEnd)
{
connectionOptions |= ConnectionOptions.Upgrade;
}
if (ReadLowerCaseUInt32(byteValue) == upgradeEnd)
{
offset += sizeof(uint) / 2;
potentialConnectionOptions = ConnectionOptions.Upgrade;
}
}
else if (tokenLength >= 5 && (*ch | 0x20) == 'c')
}
else if (c == 'c' && byteValue.Length >= sizeof(ulong))
{
if (ReadLowerCaseUInt64(byteValue) == closeEnd)
{
if ((*++ch | 0x20) == 'l' &&
(*++ch | 0x20) == 'o' &&
(*++ch | 0x20) == 's' &&
(*++ch | 0x20) == 'e')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
if (ch == tokenEnd)
{
connectionOptions |= ConnectionOptions.Close;
}
}
offset += sizeof(ulong) / 2;
potentialConnectionOptions = ConnectionOptions.Close;
}
}
tokenEnd++;
ch = tokenEnd;
if ((uint)offset >= (uint)value.Length)
{
// Consumed enitre string, move to next string.
connectionOptions |= potentialConnectionOptions;
break;
}
else
{
value = value.Slice(offset);
}
for (offset = 0; offset < value.Length; offset++)
{
c = value[offset];
if (c == ',')
{
break;
}
else if (c != ' ')
{
// Value contains extra chars; this is not the matched one.
potentialConnectionOptions = ConnectionOptions.None;
}
}
if ((uint)offset >= (uint)value.Length)
{
// Consumed enitre string, move to next string.
connectionOptions |= potentialConnectionOptions;
break;
}
else if (c == ',')
{
// Consumed value corretly.
connectionOptions |= potentialConnectionOptions;
// Skip comma.
offset++;
if ((uint)offset >= (uint)value.Length)
{
// Consumed enitre string, move to next string.
break;
}
else
{
// Move to next value.
value = value.Slice(offset);
}
}
}
}
@ -379,6 +422,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return connectionOptions;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ReadLowerCaseUInt64(ReadOnlySpan<byte> value)
=> BinaryPrimitives.ReadUInt64LittleEndian(value) | 0x0020_0020_0020_0020;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint ReadLowerCaseUInt32(ReadOnlySpan<byte> value)
=> BinaryPrimitives.ReadUInt32LittleEndian(value) | 0x0020_0020;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ushort ReadLowerCaseUInt16(ReadOnlySpan<byte> value)
=> (ushort)(BinaryPrimitives.ReadUInt16LittleEndian(value) | 0x0020);
private static char ToLowerCase(char value) => (char)(value | (char)0x0020);
public static TransferCoding GetFinalTransferCoding(StringValues transferEncoding)

View File

@ -1070,10 +1070,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
var responseHeaders = HttpResponseHeaders;
var hasConnection = responseHeaders.HasConnection;
var connectionOptions = HttpHeaders.ParseConnection(responseHeaders.HeaderConnection);
var hasTransferEncoding = responseHeaders.HasTransferEncoding;
if (_keepAlive && hasConnection && (connectionOptions & ConnectionOptions.KeepAlive) != ConnectionOptions.KeepAlive)
if (_keepAlive &&
hasConnection &&
(HttpHeaders.ParseConnection(responseHeaders.HeaderConnection) & ConnectionOptions.KeepAlive) == 0)
{
_keepAlive = false;
}

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[InlineData(",, ", (int)(ConnectionOptions.None))]
[InlineData(" , ,", (int)(ConnectionOptions.None))]
[InlineData(" , , ", (int)(ConnectionOptions.None))]
[InlineData("KEEP-ALIVE", (int)(ConnectionOptions.KeepAlive))]
[InlineData("keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData("keep-alive, upgrade", (int)(ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade))]
[InlineData("keep-alive,upgrade", (int)(ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade))]
@ -40,6 +41,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[InlineData(", ,keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData(",, keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData(", , keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData("UPGRADE", (int)(ConnectionOptions.Upgrade))]
[InlineData("upgrade", (int)(ConnectionOptions.Upgrade))]
[InlineData("upgrade,", (int)(ConnectionOptions.Upgrade))]
[InlineData("upgrade,,", (int)(ConnectionOptions.Upgrade))]
[InlineData("upgrade, ", (int)(ConnectionOptions.Upgrade))]
@ -72,6 +75,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[InlineData("u,keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData("ke,upgrade", (int)(ConnectionOptions.Upgrade))]
[InlineData("up,keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData("CLOSE", (int)(ConnectionOptions.Close))]
[InlineData("close", (int)(ConnectionOptions.Close))]
[InlineData("upgrade,close", (int)(ConnectionOptions.Close | ConnectionOptions.Upgrade))]
[InlineData("close,upgrade", (int)(ConnectionOptions.Close | ConnectionOptions.Upgrade))]
@ -87,6 +91,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[InlineData("close2 ", (int)(ConnectionOptions.None))]
[InlineData("close2 ,", (int)(ConnectionOptions.None))]
[InlineData("close2,", (int)(ConnectionOptions.None))]
[InlineData("close close", (int)(ConnectionOptions.None))]
[InlineData("close dclose", (int)(ConnectionOptions.None))]
[InlineData("keep-alivekeep-alive", (int)(ConnectionOptions.None))]
[InlineData("keep-aliveupgrade", (int)(ConnectionOptions.None))]
[InlineData("upgradeupgrade", (int)(ConnectionOptions.None))]