Consolidate HTTP charset validation logic

This commit is contained in:
John Luo 2018-07-09 16:38:44 -07:00
parent 864cfeb2aa
commit 6551eae321
17 changed files with 296 additions and 216 deletions

View File

@ -524,6 +524,9 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2ErrorInvalidPreface" xml:space="preserve">
<value>Invalid HTTP/2 connection preface.</value>
</data>
<data name="InvalidEmptyHeaderName" xml:space="preserve">
<value>Header name cannot be a null or empty string.</value>
</data>
<data name="ConnectionOrStreamAbortedByCancellationToken" xml:space="preserve">
<value>The connection or stream was aborted because a write operation was aborted with a CancellationToken.</value>
</data>

View File

@ -274,13 +274,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// This is not complete validation. It is just a quick scan for invalid characters
// but doesn't check that the target fully matches the URI spec.
for (var i = 0; i < target.Length; i++)
if (HttpCharacters.ContainsInvalidAuthorityChar(target))
{
var ch = target[i];
if (!UriUtilities.IsValidAuthorityCharacter(ch))
{
ThrowRequestTargetRejected(target);
}
ThrowRequestTargetRejected(target);
}
// The authority-form of request-target is only used for CONNECT

View File

@ -5862,7 +5862,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected override void SetValueFast(string key, in StringValues value)
{
ValidateHeaderCharacters(value);
ValidateHeaderValueCharacters(value);
switch (key.Length)
{
case 13:
@ -6167,7 +6167,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected override bool AddValueFast(string key, in StringValues value)
{
ValidateHeaderCharacters(value);
ValidateHeaderValueCharacters(value);
switch (key.Length)
{
case 13:
@ -6611,7 +6611,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
break;
}
ValidateHeaderCharacters(key);
ValidateHeaderNameCharacters(key);
Unknown.Add(key, value);
// Return true, above will throw and exit for false
return true;

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -45,6 +46,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
ThrowHeadersReadOnlyException();
}
if (string.IsNullOrEmpty(key))
{
ThrowInvalidEmtpyHeaderName();
}
if (value.Count == 0)
{
RemoveFast(key);
@ -170,6 +175,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
ThrowHeadersReadOnlyException();
}
if (string.IsNullOrEmpty(key))
{
ThrowInvalidEmtpyHeaderName();
}
if (value.Count > 0 && !AddValueFast(key, value))
{
@ -241,30 +250,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return TryGetValueFast(key, out value);
}
public static void ValidateHeaderCharacters(in StringValues headerValues)
public static void ValidateHeaderValueCharacters(in StringValues headerValues)
{
var count = headerValues.Count;
for (var i = 0; i < count; i++)
{
ValidateHeaderCharacters(headerValues[i]);
ValidateHeaderValueCharacters(headerValues[i]);
}
}
public static void ValidateHeaderCharacters(string headerCharacters)
public static void ValidateHeaderValueCharacters(string headerCharacters)
{
if (headerCharacters != null)
{
foreach (var ch in headerCharacters)
var invalid = HttpCharacters.IndexOfInvalidFieldValueChar(headerCharacters);
if (invalid >= 0)
{
if (ch < 0x20 || ch > 0x7E)
{
ThrowInvalidHeaderCharacter(ch);
}
ThrowInvalidHeaderCharacter(headerCharacters[invalid]);
}
}
}
public static void ValidateHeaderNameCharacters(string headerCharacters)
{
var invalid = HttpCharacters.IndexOfInvalidTokenChar(headerCharacters);
if (invalid >= 0)
{
ThrowInvalidHeaderCharacter(headerCharacters[invalid]);
}
}
public static unsafe ConnectionOptions ParseConnection(in StringValues connection)
{
var connectionOptions = ConnectionOptions.None;
@ -440,5 +456,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
throw new InvalidOperationException(CoreStrings.FormatInvalidAsciiOrControlChar(string.Format("0x{0:X4}", (ushort)ch)));
}
private static void ThrowInvalidEmtpyHeaderName()
{
throw new InvalidOperationException(CoreStrings.InvalidEmptyHeaderName);
}
}
}

View File

@ -363,6 +363,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var valueEnd = length - 3;
var nameEnd = FindEndOfName(headerLine, length);
// Header name is empty
if (nameEnd == 0)
{
RejectRequestHeader(headerLine, length);
}
if (headerLine[valueEnd + 2] != ByteLF)
{
RejectRequestHeader(headerLine, length);
@ -437,55 +443,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe Span<byte> GetUnknownMethod(byte* data, int length, out int methodLength)
{
methodLength = 0;
for (var i = 0; i < length; i++)
var invalidIndex = HttpCharacters.IndexOfInvalidTokenChar(data, length);
if (invalidIndex <= 0 || data[invalidIndex] != ByteSpace)
{
var ch = data[i];
if (ch == ByteSpace)
{
if (i == 0)
{
RejectRequestLine(data, length);
}
methodLength = i;
break;
}
else if (!IsValidTokenChar((char)ch))
{
RejectRequestLine(data, length);
}
RejectRequestLine(data, length);
}
methodLength = invalidIndex;
return new Span<byte>(data, methodLength);
}
private static bool IsValidTokenChar(char c)
{
// Determines if a character is valid as a 'token' as defined in the
// HTTP spec: https://tools.ietf.org/html/rfc7230#section-3.2.6
return
(c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
c == '!' ||
c == '#' ||
c == '$' ||
c == '%' ||
c == '&' ||
c == '\'' ||
c == '*' ||
c == '+' ||
c == '-' ||
c == '.' ||
c == '^' ||
c == '_' ||
c == '`' ||
c == '|' ||
c == '~';
}
[StackTraceHidden]
private unsafe void RejectRequestLine(byte* requestLine, int length)
=> throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length);

View File

@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
[MethodImpl(MethodImplOptions.NoInlining)]
private void SetValueUnknown(string key, in StringValues value)
{
ValidateHeaderCharacters(key);
ValidateHeaderNameCharacters(key);
Unknown[key] = value;
}

View File

@ -8,8 +8,6 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
{
internal class UrlDecoder
{
static bool[] IsAllowed = new bool[0x7F + 1];
/// <summary>
/// Unescape a URL path
/// </summary>
@ -334,78 +332,5 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
return false;
}
static UrlDecoder()
{
// Unreserved
IsAllowed['A'] = true;
IsAllowed['B'] = true;
IsAllowed['C'] = true;
IsAllowed['D'] = true;
IsAllowed['E'] = true;
IsAllowed['F'] = true;
IsAllowed['G'] = true;
IsAllowed['H'] = true;
IsAllowed['I'] = true;
IsAllowed['J'] = true;
IsAllowed['K'] = true;
IsAllowed['L'] = true;
IsAllowed['M'] = true;
IsAllowed['N'] = true;
IsAllowed['O'] = true;
IsAllowed['P'] = true;
IsAllowed['Q'] = true;
IsAllowed['R'] = true;
IsAllowed['S'] = true;
IsAllowed['T'] = true;
IsAllowed['U'] = true;
IsAllowed['V'] = true;
IsAllowed['W'] = true;
IsAllowed['X'] = true;
IsAllowed['Y'] = true;
IsAllowed['Z'] = true;
IsAllowed['a'] = true;
IsAllowed['b'] = true;
IsAllowed['c'] = true;
IsAllowed['d'] = true;
IsAllowed['e'] = true;
IsAllowed['f'] = true;
IsAllowed['g'] = true;
IsAllowed['h'] = true;
IsAllowed['i'] = true;
IsAllowed['j'] = true;
IsAllowed['k'] = true;
IsAllowed['l'] = true;
IsAllowed['m'] = true;
IsAllowed['n'] = true;
IsAllowed['o'] = true;
IsAllowed['p'] = true;
IsAllowed['q'] = true;
IsAllowed['r'] = true;
IsAllowed['s'] = true;
IsAllowed['t'] = true;
IsAllowed['u'] = true;
IsAllowed['v'] = true;
IsAllowed['w'] = true;
IsAllowed['x'] = true;
IsAllowed['y'] = true;
IsAllowed['z'] = true;
IsAllowed['0'] = true;
IsAllowed['1'] = true;
IsAllowed['2'] = true;
IsAllowed['3'] = true;
IsAllowed['4'] = true;
IsAllowed['5'] = true;
IsAllowed['6'] = true;
IsAllowed['7'] = true;
IsAllowed['8'] = true;
IsAllowed['9'] = true;
IsAllowed['-'] = true;
IsAllowed['_'] = true;
IsAllowed['.'] = true;
IsAllowed['~'] = true;
}
}
}

View File

@ -0,0 +1,202 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
internal static class HttpCharacters
{
private static readonly int _tableSize = 128;
private static readonly bool[] _alphaNumeric = InitializeAlphaNumeric();
private static readonly bool[] _authority = InitializeAuthority();
private static readonly bool[] _token = InitializeToken();
private static readonly bool[] _host = InitializeHost();
private static readonly bool[] _fieldValue = InitializeFieldValue();
internal static void Initialize()
{
// Access _alphaNumeric to initialize static fields
var initialize = _alphaNumeric;
}
private static bool[] InitializeAlphaNumeric()
{
// ALPHA and DIGIT https://tools.ietf.org/html/rfc5234#appendix-B.1
var alphaNumeric = new bool[_tableSize];
for (var c = '0'; c <= '9'; c++)
{
alphaNumeric[c] = true;
}
for (var c = 'A'; c <= 'Z'; c++)
{
alphaNumeric[c] = true;
}
for (var c = 'a'; c <= 'z'; c++)
{
alphaNumeric[c] = true;
}
return alphaNumeric;
}
private static bool[] InitializeAuthority()
{
// Authority https://tools.ietf.org/html/rfc3986#section-3.2
// Examples:
// microsoft.com
// hostname:8080
// [::]:8080
// [fe80::]
// 127.0.0.1
// user@host.com
// user:password@host.com
var authority = new bool[_tableSize];
Array.Copy(_alphaNumeric, authority, _tableSize);
authority[':'] = true;
authority['.'] = true;
authority['['] = true;
authority[']'] = true;
authority['@'] = true;
return authority;
}
private static bool[] InitializeToken()
{
// tchar https://tools.ietf.org/html/rfc7230#appendix-B
var token = new bool[_tableSize];
Array.Copy(_alphaNumeric, token, _tableSize);
token['!'] = true;
token['#'] = true;
token['$'] = true;
token['%'] = true;
token['&'] = true;
token['\''] = true;
token['*'] = true;
token['+'] = true;
token['-'] = true;
token['.'] = true;
token['^'] = true;
token['_'] = true;
token['`'] = true;
token['|'] = true;
token['~'] = true;
return token;
}
private static bool[] InitializeHost()
{
// Matches Http.Sys
// Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys
var host = new bool[_tableSize];
Array.Copy(_alphaNumeric, host, _tableSize);
host['!'] = true;
host['$'] = true;
host['&'] = true;
host['\''] = true;
host['('] = true;
host[')'] = true;
host['-'] = true;
host['.'] = true;
host['_'] = true;
host['~'] = true;
return host;
}
private static bool[] InitializeFieldValue()
{
// field-value https://tools.ietf.org/html/rfc7230#section-3.2
var fieldValue = new bool[_tableSize];
for (var c = 0x20; c <= 0x7e; c++) // VCHAR and SP
{
fieldValue[c] = true;
}
return fieldValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ContainsInvalidAuthorityChar(Span<byte> s)
{
var authority = _authority;
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
if (c >= (uint)authority.Length || !authority[c])
{
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOfInvalidHostChar(string s)
{
var host = _host;
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
if (c >= (uint)host.Length || !host[c])
{
return i;
}
}
return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOfInvalidTokenChar(string s)
{
var token = _token;
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
if (c >= (uint)token.Length || !token[c])
{
return i;
}
}
return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe static int IndexOfInvalidTokenChar(byte* s, int length)
{
var token = _token;
for (var i = 0; i < length; i++)
{
var c = s[i];
if (c >= (uint)token.Length || !token[c])
{
return i;
}
}
return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOfInvalidFieldValueChar(string s)
{
var fieldValue = _fieldValue;
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
if (c >= (uint)fieldValue.Length || !fieldValue[c])
{
return i;
}
}
return -1;
}
}
}

View File

@ -51,7 +51,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
SetKnownMethod(_mask8Chars, _httpConnectMethodLong, HttpMethod.Connect, 7);
SetKnownMethod(_mask8Chars, _httpOptionsMethodLong, HttpMethod.Options, 7);
FillKnownMethodsGaps();
InitializeHostCharValidity();
_methodNames[(byte)HttpMethod.Connect] = HttpMethods.Connect;
_methodNames[(byte)HttpMethod.Delete] = HttpMethods.Delete;
_methodNames[(byte)HttpMethod.Get] = HttpMethods.Get;

View File

@ -13,8 +13,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public static partial class HttpUtilities
{
private static readonly bool[] HostCharValidity = new bool[127];
public const string Http10Version = "HTTP/1.0";
public const string Http11Version = "HTTP/1.1";
public const string Http2Version = "HTTP/2";
@ -31,35 +29,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
// Only called from the static constructor
private static void InitializeHostCharValidity()
{
// Matches Http.Sys
// Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys
HostCharValidity['!'] = true;
HostCharValidity['$'] = true;
HostCharValidity['&'] = true;
HostCharValidity['\''] = true;
HostCharValidity['('] = true;
HostCharValidity[')'] = true;
HostCharValidity['-'] = true;
HostCharValidity['.'] = true;
HostCharValidity['_'] = true;
HostCharValidity['~'] = true;
for (var ch = '0'; ch <= '9'; ch++)
{
HostCharValidity[ch] = true;
}
for (var ch = 'A'; ch <= 'Z'; ch++)
{
HostCharValidity[ch] = true;
}
for (var ch = 'a'; ch <= 'z'; ch++)
{
HostCharValidity[ch] = true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
{
@ -448,16 +417,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
}
// Enregister array
var hostCharValidity = HostCharValidity;
for (var i = 0; i < hostText.Length; i++)
var invalid = HttpCharacters.IndexOfInvalidHostChar(hostText);
if (invalid >= 0)
{
if (!hostCharValidity[hostText[i]])
{
// Tail call
ValidateHostPort(hostText, i);
return;
}
// Tail call
ValidateHostPort(hostText, invalid);
}
}
}

View File

@ -1,35 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public class UriUtilities
{
/// <summary>
/// Returns true if character is valid in the 'authority' section of a URI.
/// <see href="https://tools.ietf.org/html/rfc3986#section-3.2"/>
/// </summary>
/// <param name="ch">The character</param>
/// <returns></returns>
public static bool IsValidAuthorityCharacter(byte ch)
{
// Examples:
// microsoft.com
// hostname:8080
// [::]:8080
// [fe80::]
// 127.0.0.1
// user@host.com
// user:password@host.com
return
(ch >= '0' && ch <= '9') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
ch == ':' ||
ch == '.' ||
ch == '[' ||
ch == ']' ||
ch == '@';
}
}
}

View File

@ -57,6 +57,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
Features = new FeatureCollection();
_serverAddresses = new ServerAddressesFeature();
Features.Set(_serverAddresses);
HttpCharacters.Initialize();
}
private static ServiceContext CreateServiceContext(IOptions<KestrelServerOptions> options, ILoggerFactory loggerFactory)

View File

@ -1904,6 +1904,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2ErrorInvalidPreface()
=> GetString("Http2ErrorInvalidPreface");
/// <summary>
/// Header name cannot be a null or empty string.
/// </summary>
internal static string InvalidEmptyHeaderName
{
get => GetString("InvalidEmptyHeaderName");
}
/// <summary>
/// Header name cannot be a null or empty string.
/// </summary>
internal static string FormatInvalidEmptyHeaderName()
=> GetString("InvalidEmptyHeaderName");
/// <summary>
/// The connection or stream was aborted because a write operation was aborted with a CancellationToken.
/// </summary>

View File

@ -79,6 +79,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[InlineData("Server", "Dašta")]
[InlineData("Unknownš-Header", "Data")]
[InlineData("Seršver", "Data")]
[InlineData("Server\"", "Data")]
[InlineData("Server(", "Data")]
[InlineData("Server)", "Data")]
[InlineData("Server,", "Data")]
[InlineData("Server/", "Data")]
[InlineData("Server:", "Data")]
[InlineData("Server;", "Data")]
[InlineData("Server<", "Data")]
[InlineData("Server=", "Data")]
[InlineData("Server>", "Data")]
[InlineData("Server?", "Data")]
[InlineData("Server@", "Data")]
[InlineData("Server[", "Data")]
[InlineData("Server\\", "Data")]
[InlineData("Server]", "Data")]
[InlineData("Server{", "Data")]
[InlineData("Server}", "Data")]
[InlineData("", "Data")]
[InlineData(null, "Data")]
public void AddingControlOrNonAsciiCharactersToHeadersThrows(string key, string value)
{
var responseHeaders = new HttpResponseHeaders();

View File

@ -428,6 +428,9 @@ namespace Microsoft.AspNetCore.Testing
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r\r", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF },
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF },
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF },
// Empty header name
new[] { ": value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@": value\x0D\x0A") },
};
public static TheoryData<string, string> HostHeaderData

View File

@ -84,7 +84,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{{
{4}
FillKnownMethodsGaps();
InitializeHostCharValidity();
{5}
}}

View File

@ -403,7 +403,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected override void SetValueFast(string key, in StringValues value)
{{{(loop.ClassName == "HttpResponseHeaders" ? @"
ValidateHeaderCharacters(value);" : "")}
ValidateHeaderValueCharacters(value);" : "")}
switch (key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
@ -425,7 +425,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected override bool AddValueFast(string key, in StringValues value)
{{{(loop.ClassName == "HttpResponseHeaders" ? @"
ValidateHeaderCharacters(value);" : "")}
ValidateHeaderValueCharacters(value);" : "")}
switch (key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
@ -451,7 +451,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
break;")}
}}
{(loop.ClassName == "HttpResponseHeaders" ? @"
ValidateHeaderCharacters(key);" : "")}
ValidateHeaderNameCharacters(key);" : "")}
Unknown.Add(key, value);
// Return true, above will throw and exit for false
return true;