272 lines
11 KiB
C#
272 lines
11 KiB
C#
// 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.Diagnostics.Contracts;
|
|
using System.Text;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.Formatters.Internal
|
|
{
|
|
public static class HttpTokenParsingRules
|
|
{
|
|
private static readonly bool[] TokenChars;
|
|
private const int MaxNestedCount = 5;
|
|
|
|
internal const char CR = '\r';
|
|
internal const char LF = '\n';
|
|
internal const char SP = ' ';
|
|
internal const char Tab = '\t';
|
|
internal const int MaxInt64Digits = 19;
|
|
internal const int MaxInt32Digits = 10;
|
|
|
|
// iso-8859-1, Western European (ISO)
|
|
internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding(28591);
|
|
|
|
static HttpTokenParsingRules()
|
|
{
|
|
// token = 1*<any CHAR except CTLs or separators>
|
|
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
|
|
|
TokenChars = new bool[128]; // everything is false
|
|
|
|
for (int i = 33; i < 127; i++) // skip Space (32) & DEL (127)
|
|
{
|
|
TokenChars[i] = true;
|
|
}
|
|
|
|
// remove separators: these are not valid token characters
|
|
TokenChars[(byte)'('] = false;
|
|
TokenChars[(byte)')'] = false;
|
|
TokenChars[(byte)'<'] = false;
|
|
TokenChars[(byte)'>'] = false;
|
|
TokenChars[(byte)'@'] = false;
|
|
TokenChars[(byte)','] = false;
|
|
TokenChars[(byte)';'] = false;
|
|
TokenChars[(byte)':'] = false;
|
|
TokenChars[(byte)'\\'] = false;
|
|
TokenChars[(byte)'"'] = false;
|
|
TokenChars[(byte)'/'] = false;
|
|
TokenChars[(byte)'['] = false;
|
|
TokenChars[(byte)']'] = false;
|
|
TokenChars[(byte)'?'] = false;
|
|
TokenChars[(byte)'='] = false;
|
|
TokenChars[(byte)'{'] = false;
|
|
TokenChars[(byte)'}'] = false;
|
|
}
|
|
|
|
internal static bool IsTokenChar(char character)
|
|
{
|
|
// Must be between 'space' (32) and 'DEL' (127)
|
|
if (character > 127)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return TokenChars[character];
|
|
}
|
|
|
|
internal static int GetTokenLength(string input, int startIndex)
|
|
{
|
|
Contract.Requires(input != null);
|
|
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
|
|
|
|
if (startIndex >= input.Length)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var current = startIndex;
|
|
|
|
while (current < input.Length)
|
|
{
|
|
if (!IsTokenChar(input[current]))
|
|
{
|
|
return current - startIndex;
|
|
}
|
|
current++;
|
|
}
|
|
return input.Length - startIndex;
|
|
}
|
|
|
|
internal static int GetWhitespaceLength(string input, int startIndex)
|
|
{
|
|
Contract.Requires(input != null);
|
|
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
|
|
|
|
if (startIndex >= input.Length)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var current = startIndex;
|
|
|
|
char c;
|
|
while (current < input.Length)
|
|
{
|
|
c = input[current];
|
|
|
|
if ((c == SP) || (c == Tab))
|
|
{
|
|
current++;
|
|
continue;
|
|
}
|
|
|
|
if (c == CR)
|
|
{
|
|
// If we have a #13 char, it must be followed by #10 and then at least one SP or HT.
|
|
if ((current + 2 < input.Length) && (input[current + 1] == LF))
|
|
{
|
|
char spaceOrTab = input[current + 2];
|
|
if ((spaceOrTab == SP) || (spaceOrTab == Tab))
|
|
{
|
|
current += 3;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return current - startIndex;
|
|
}
|
|
|
|
// All characters between startIndex and the end of the string are LWS characters.
|
|
return input.Length - startIndex;
|
|
}
|
|
|
|
internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length)
|
|
{
|
|
var nestedCount = 0;
|
|
return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length);
|
|
}
|
|
|
|
// quoted-pair = "\" CHAR
|
|
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length)
|
|
{
|
|
Contract.Requires(input != null);
|
|
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
|
|
Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) &&
|
|
(Contract.ValueAtReturn(out length) <= (input.Length - startIndex)));
|
|
|
|
length = 0;
|
|
|
|
if (input[startIndex] != '\\')
|
|
{
|
|
return HttpParseResult.NotParsed;
|
|
}
|
|
|
|
// Quoted-char has 2 characters. Check wheter there are 2 chars left ('\' + char)
|
|
// If so, check whether the character is in the range 0-127. If not, it's an invalid value.
|
|
if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127))
|
|
{
|
|
return HttpParseResult.InvalidFormat;
|
|
}
|
|
|
|
// We don't care what the char next to '\' is.
|
|
length = 2;
|
|
return HttpParseResult.Parsed;
|
|
}
|
|
|
|
// TEXT = <any OCTET except CTLs, but including LWS>
|
|
// LWS = [CRLF] 1*( SP | HT )
|
|
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
|
//
|
|
// Since we don't really care about the content of a quoted string or comment, we're more tolerant and
|
|
// allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment).
|
|
//
|
|
// 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like
|
|
// "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested
|
|
// comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any)
|
|
// is unusual.
|
|
private static HttpParseResult GetExpressionLength(
|
|
string input,
|
|
int startIndex,
|
|
char openChar,
|
|
char closeChar,
|
|
bool supportsNesting,
|
|
ref int nestedCount,
|
|
out int length)
|
|
{
|
|
Contract.Requires(input != null);
|
|
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
|
|
Contract.Ensures((Contract.Result<HttpParseResult>() != HttpParseResult.Parsed) ||
|
|
(Contract.ValueAtReturn<int>(out length) > 0));
|
|
|
|
length = 0;
|
|
|
|
if (input[startIndex] != openChar)
|
|
{
|
|
return HttpParseResult.NotParsed;
|
|
}
|
|
|
|
var current = startIndex + 1; // Start parsing with the character next to the first open-char
|
|
while (current < input.Length)
|
|
{
|
|
// Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e.
|
|
// quoted char + closing char). Otherwise the closing char may be considered part of the quoted char.
|
|
var quotedPairLength = 0;
|
|
if ((current + 2 < input.Length) &&
|
|
(GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed))
|
|
{
|
|
// We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair,
|
|
// but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only
|
|
// allows ASCII chars after '\'; qdtext allows both '\' and >127 chars).
|
|
current = current + quotedPairLength;
|
|
continue;
|
|
}
|
|
|
|
// If we support nested expressions and we find an open-char, then parse the nested expressions.
|
|
if (supportsNesting && (input[current] == openChar))
|
|
{
|
|
nestedCount++;
|
|
try
|
|
{
|
|
// Check if we exceeded the number of nested calls.
|
|
if (nestedCount > MaxNestedCount)
|
|
{
|
|
return HttpParseResult.InvalidFormat;
|
|
}
|
|
|
|
var nestedLength = 0;
|
|
var nestedResult = GetExpressionLength(input, current, openChar, closeChar,
|
|
supportsNesting, ref nestedCount, out nestedLength);
|
|
|
|
switch (nestedResult)
|
|
{
|
|
case HttpParseResult.Parsed:
|
|
current += nestedLength; // add the length of the nested expression and continue.
|
|
break;
|
|
|
|
case HttpParseResult.NotParsed:
|
|
Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " +
|
|
"parsing, because we found the open-char. So either it's a valid nested " +
|
|
"expression or it has invalid format.");
|
|
break;
|
|
|
|
case HttpParseResult.InvalidFormat:
|
|
// If the nested expression is invalid, we can't continue, so we fail with invalid format.
|
|
return HttpParseResult.InvalidFormat;
|
|
|
|
default:
|
|
Contract.Assert(false, "Unknown enum result: " + nestedResult);
|
|
break;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
nestedCount--;
|
|
}
|
|
}
|
|
|
|
if (input[current] == closeChar)
|
|
{
|
|
length = current - startIndex + 1;
|
|
return HttpParseResult.Parsed;
|
|
}
|
|
current++;
|
|
}
|
|
|
|
// We didn't see the final quote, therefore we have an invalid expression string.
|
|
return HttpParseResult.InvalidFormat;
|
|
}
|
|
}
|
|
}
|