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); var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection);
upgrade = (connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade; upgrade = (connectionOptions & ConnectionOptions.Upgrade) != 0;
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive; keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) != 0;
} }
if (upgrade) if (upgrade)

View File

@ -8,11 +8,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http 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 connectionOptions = ConnectionOptions.None;
var connectionCount = connection.Count; var connectionCount = connection.Count;
for (var i = 0; i < connectionCount; i++) for (var i = 0; i < connectionCount; i++)
{ {
var value = connection[i]; var value = connection[i].AsSpan();
fixed (char* ptr = value) while (value.Length > 0)
{ {
var ch = ptr; int offset;
var tokenEnd = ch; char c = '\0';
var end = ch + value.Length; // Skip any spaces and empty values.
for (offset = 0; offset < value.Length; offset++)
while (ch < end)
{ {
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 (ReadLowerCaseUInt64(byteValue) == keepAliveMiddle)
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')
{ {
ch++; offset += sizeof(ulong) / 2;
while (ch < tokenEnd && *ch == ' ') byteValue = byteValue.Slice(sizeof(ulong));
{
ch++;
}
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' && offset += sizeof(ulong) / 2;
(*++ch | 0x20) == 'g' && byteValue = byteValue.Slice(sizeof(ulong));
(*++ch | 0x20) == 'r' &&
(*++ch | 0x20) == 'a' &&
(*++ch | 0x20) == 'd' &&
(*++ch | 0x20) == 'e')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
if (ch == tokenEnd) if (ReadLowerCaseUInt32(byteValue) == upgradeEnd)
{ {
connectionOptions |= ConnectionOptions.Upgrade; 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' && offset += sizeof(ulong) / 2;
(*++ch | 0x20) == 'o' && potentialConnectionOptions = ConnectionOptions.Close;
(*++ch | 0x20) == 's' &&
(*++ch | 0x20) == 'e')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
if (ch == tokenEnd)
{
connectionOptions |= ConnectionOptions.Close;
}
}
} }
}
tokenEnd++; if ((uint)offset >= (uint)value.Length)
ch = tokenEnd; {
// 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; 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); private static char ToLowerCase(char value) => (char)(value | (char)0x0020);
public static TransferCoding GetFinalTransferCoding(StringValues transferEncoding) public static TransferCoding GetFinalTransferCoding(StringValues transferEncoding)

View File

@ -1070,10 +1070,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
var responseHeaders = HttpResponseHeaders; var responseHeaders = HttpResponseHeaders;
var hasConnection = responseHeaders.HasConnection; var hasConnection = responseHeaders.HasConnection;
var connectionOptions = HttpHeaders.ParseConnection(responseHeaders.HeaderConnection);
var hasTransferEncoding = responseHeaders.HasTransferEncoding; var hasTransferEncoding = responseHeaders.HasTransferEncoding;
if (_keepAlive && hasConnection && (connectionOptions & ConnectionOptions.KeepAlive) != ConnectionOptions.KeepAlive) if (_keepAlive &&
hasConnection &&
(HttpHeaders.ParseConnection(responseHeaders.HeaderConnection) & ConnectionOptions.KeepAlive) == 0)
{ {
_keepAlive = false; _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(" , ,", (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", (int)(ConnectionOptions.KeepAlive))]
[InlineData("keep-alive, upgrade", (int)(ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade))] [InlineData("keep-alive, upgrade", (int)(ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade))]
[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(",, 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))] [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("u,keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData("ke,upgrade", (int)(ConnectionOptions.Upgrade))] [InlineData("ke,upgrade", (int)(ConnectionOptions.Upgrade))]
[InlineData("up,keep-alive", (int)(ConnectionOptions.KeepAlive))] [InlineData("up,keep-alive", (int)(ConnectionOptions.KeepAlive))]
[InlineData("CLOSE", (int)(ConnectionOptions.Close))]
[InlineData("close", (int)(ConnectionOptions.Close))] [InlineData("close", (int)(ConnectionOptions.Close))]
[InlineData("upgrade,close", (int)(ConnectionOptions.Close | ConnectionOptions.Upgrade))] [InlineData("upgrade,close", (int)(ConnectionOptions.Close | ConnectionOptions.Upgrade))]
[InlineData("close,upgrade", (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("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-alivekeep-alive", (int)(ConnectionOptions.None))]
[InlineData("keep-aliveupgrade", (int)(ConnectionOptions.None))] [InlineData("keep-aliveupgrade", (int)(ConnectionOptions.None))]
[InlineData("upgradeupgrade", (int)(ConnectionOptions.None))] [InlineData("upgradeupgrade", (int)(ConnectionOptions.None))]