Don't decode forward slashes in request path (#146).

This commit is contained in:
Cesar Blum Silveira 2015-12-16 13:31:33 -08:00
Родитель 905b5bcfc2
Коммит d9e06f8e6e
5 изменённых файлов: 130 добавлений и 319 удалений

Просмотреть файл

@ -1,3 +1,3 @@
{
"projects": ["src"]
"projects": ["src", "test"]
}

Просмотреть файл

@ -109,21 +109,21 @@ namespace Microsoft.Net.Http.Server
}
UrlPrefix prefix = httpContext.Server.UrlPrefixes.GetPrefix((int)_contextId);
string orriginalPath = RequestPath;
string originalPath = RequestPath;
// These paths are both unescaped already.
if (orriginalPath.Length == prefix.Path.Length - 1)
if (originalPath.Length == prefix.Path.Length - 1)
{
// They matched exactly except for the trailing slash.
_pathBase = orriginalPath;
_pathBase = originalPath;
_path = string.Empty;
}
else
{
// url: /base/path, prefix: /base/, base: /base, path: /path
// url: /, prefix: /, base: , path: /
_pathBase = orriginalPath.Substring(0, prefix.Path.Length - 1);
_path = orriginalPath.Substring(prefix.Path.Length - 1);
_pathBase = originalPath.Substring(0, prefix.Path.Length - 1);
_path = originalPath.Substring(prefix.Path.Length - 1);
}
int major = memoryBlob.RequestBlob->Version.MajorVersion;
@ -386,21 +386,7 @@ namespace Microsoft.Net.Http.Server
{
get { return IsSecureConnection ? Constants.HttpsScheme : Constants.HttpScheme; }
}
/*
internal Uri RequestUri
{
get
{
if (_requestUri == null)
{
_requestUri = RequestUriBuilder.GetRequestUri(
_rawUrl, RequestScheme, _cookedUrlHost, _cookedUrlPath, _cookedUrlQuery);
}
return _requestUri;
}
}
*/
internal string RequestPath
{
get

Просмотреть файл

@ -32,64 +32,29 @@ namespace Microsoft.Net.Http.Server
// We don't use the cooked URL because http.sys unescapes all percent-encoded values. However,
// we also can't just use the raw Uri, since http.sys supports not only Utf-8, but also ANSI/DBCS and
// Unicode code points. System.Uri only supports Utf-8.
// The purpose of this class is to convert all ANSI, DBCS, and Unicode code points into percent encoded
// Utf-8 characters.
// The purpose of this class is to decode all UTF-8 percent encoded characters, with the
// exception of %2F ('/'), which is left encoded.
internal sealed class RequestUriBuilder
{
private static readonly bool UseCookedRequestUrl;
private static readonly Encoding Utf8Encoding;
private static readonly Encoding AnsiEncoding;
private readonly string _rawUri;
private readonly string _cookedUriScheme;
private readonly string _cookedUriHost;
private readonly string _cookedUriPath;
private readonly string _cookedUriQuery;
// This field is used to build the final request Uri string from the Uri parts passed to the ctor.
private StringBuilder _requestUriString;
// The raw path is parsed by looping through all characters from left to right. 'rawOctets'
// is used to store consecutive percent encoded octets as actual byte values: e.g. for path /pa%C3%84th%2F/
// rawOctets will be set to { 0xC3, 0x84 } when we reach character 't' and it will be { 0x2F } when
// is used to store consecutive percent encoded octets as actual byte values: e.g. for path /pa%C3%84th%20/
// rawOctets will be set to { 0xC3, 0x84 } when we reach character 't' and it will be { 0x20 } when
// we reach the final '/'. I.e. after a sequence of percent encoded octets ends, we use rawOctets as
// input to the encoding and percent encode the resulting string into UTF-8 octets.
//
// When parsing ANSI (Latin 1) encoded path '/pa%C4th/', %C4 will be added to rawOctets and when
// we reach 't', the content of rawOctets { 0xC4 } will be fed into the ANSI encoding. The resulting
// string '�' will be percent encoded into UTF-8 octets and appended to requestUriString. The final
// path will be '/pa%C3%84th/', where '%C3%84' is the UTF-8 percent encoded character '�'.
// input to the encoding and decode them into a string.
private List<byte> _rawOctets;
private string _rawPath;
// Holds the final request Uri.
private Uri _requestUri;
static RequestUriBuilder()
{
// TODO: False triggers more detailed/correct parsing, but it's rather slow.
UseCookedRequestUrl = true; // SettingsSectionInternal.Section.HttpListenerUnescapeRequestUrl;
Utf8Encoding = new UTF8Encoding(false, true);
#if DOTNET5_4
AnsiEncoding = Utf8Encoding;
#else
AnsiEncoding = Encoding.GetEncoding(0, new EncoderExceptionFallback(), new DecoderExceptionFallback());
#endif
}
private RequestUriBuilder(string rawUri, string cookedUriScheme, string cookedUriHost,
string cookedUriPath, string cookedUriQuery)
{
Debug.Assert(!string.IsNullOrEmpty(rawUri), "Empty raw URL.");
Debug.Assert(!string.IsNullOrEmpty(cookedUriScheme), "Empty cooked URL scheme.");
Debug.Assert(!string.IsNullOrEmpty(cookedUriHost), "Empty cooked URL host.");
Debug.Assert(!string.IsNullOrEmpty(cookedUriPath), "Empty cooked URL path.");
this._rawUri = rawUri;
this._cookedUriScheme = cookedUriScheme;
this._cookedUriHost = cookedUriHost;
this._cookedUriPath = AddSlashToAsteriskOnlyPath(cookedUriPath);
this._cookedUriQuery = cookedUriQuery ?? string.Empty;
}
private RequestUriBuilder(string rawUri, string cookedUriPath)
@ -98,10 +63,7 @@ namespace Microsoft.Net.Http.Server
Debug.Assert(!string.IsNullOrEmpty(cookedUriPath), "Empty cooked URL path.");
this._rawUri = rawUri;
this._cookedUriScheme = string.Empty;
this._cookedUriHost = string.Empty;
this._cookedUriPath = AddSlashToAsteriskOnlyPath(cookedUriPath);
this._cookedUriQuery = string.Empty;
}
private enum ParsingResult
@ -111,48 +73,6 @@ namespace Microsoft.Net.Http.Server
EncodingError
}
private enum EncodingType
{
Primary,
Secondary
}
public static Uri GetRequestUri(string rawUri, string cookedUriScheme, string cookedUriHost,
string cookedUriPath, string cookedUriQuery)
{
RequestUriBuilder builder = new RequestUriBuilder(rawUri,
cookedUriScheme, cookedUriHost, cookedUriPath, cookedUriQuery);
return builder.Build();
}
private Uri Build()
{
// if the user enabled the "use raw Uri" setting in <httpListener> section, we'll use the raw
// path rather than the cooked path.
if (UseCookedRequestUrl)
{
// corresponds to pre-4.0 behavior: use the cooked URI.
BuildRequestUriUsingCookedPath();
if (_requestUri == null)
{
BuildRequestUriUsingRawPath();
}
}
else
{
BuildRequestUriUsingRawPath();
if (_requestUri == null)
{
BuildRequestUriUsingCookedPath();
}
}
return _requestUri;
}
// Process only the path.
internal static string GetRequestPath(string rawUri, string cookedUriPath)
{
@ -163,11 +83,6 @@ namespace Microsoft.Net.Http.Server
private string GetPath()
{
if (UseCookedRequestUrl)
{
return _cookedUriPath;
}
// Initialize 'rawPath' only if really needed; i.e. if we build the request Uri from the raw Uri.
_rawPath = GetPath(_rawUri);
@ -181,18 +96,10 @@ namespace Microsoft.Net.Http.Server
return _rawPath;
}
// Try to check the raw path using first the primary encoding (according to http.sys settings);
// if it fails try the secondary encoding.
_rawOctets = new List<byte>();
_requestUriString = new StringBuilder();
ParsingResult result = ParseRawPath(GetEncoding(EncodingType.Primary));
if (result == ParsingResult.EncodingError)
{
_rawOctets = new List<byte>();
_requestUriString = new StringBuilder();
result = ParseRawPath(GetEncoding(EncodingType.Secondary));
}
ParsingResult result = ParseRawPath(Utf8Encoding);
if (result == ParsingResult.Success)
{
return _requestUriString.ToString();
@ -202,115 +109,6 @@ namespace Microsoft.Net.Http.Server
return _cookedUriPath;
}
private void BuildRequestUriUsingCookedPath()
{
bool isValid = Uri.TryCreate(_cookedUriScheme + Constants.SchemeDelimiter + _cookedUriHost + _cookedUriPath +
_cookedUriQuery, UriKind.Absolute, out _requestUri);
// Creating a Uri from the cooked Uri should really always work: If not, we log at least.
if (!isValid)
{
LogWarning("BuildRequestUriUsingCookedPath", "Unable to create URI: " + _cookedUriScheme + Constants.SchemeDelimiter +
_cookedUriHost + _cookedUriPath + _cookedUriQuery);
}
}
private void BuildRequestUriUsingRawPath()
{
bool isValid = false;
// Initialize 'rawPath' only if really needed; i.e. if we build the request Uri from the raw Uri.
_rawPath = GetPath(_rawUri);
// If HTTP.sys only parses Utf-8, we can safely use the raw path: it must be a valid Utf-8 string.
if (!HttpSysSettings.EnableNonUtf8 || string.IsNullOrEmpty(_rawPath))
{
string path = _rawPath;
if (string.IsNullOrEmpty(path))
{
path = "/";
Debug.Assert(string.IsNullOrEmpty(_cookedUriQuery),
"Query is only allowed if there is a non-empty path. At least '/' path required.");
}
isValid = Uri.TryCreate(_cookedUriScheme + Constants.SchemeDelimiter + _cookedUriHost + path + _cookedUriQuery,
UriKind.Absolute, out _requestUri);
}
else
{
// Try to check the raw path using first the primary encoding (according to http.sys settings);
// if it fails try the secondary encoding.
ParsingResult result = BuildRequestUriUsingRawPath(GetEncoding(EncodingType.Primary));
if (result == ParsingResult.EncodingError)
{
Encoding secondaryEncoding = GetEncoding(EncodingType.Secondary);
result = BuildRequestUriUsingRawPath(secondaryEncoding);
}
isValid = (result == ParsingResult.Success) ? true : false;
}
// Log that we weren't able to create a Uri from the raw string.
if (!isValid)
{
LogWarning("BuildRequestUriUsingRawPath", "Unable to create Uri: " + _cookedUriScheme + Constants.SchemeDelimiter
+ _cookedUriHost + _rawPath + _cookedUriQuery);
}
}
private static Encoding GetEncoding(EncodingType type)
{
Debug.Assert(HttpSysSettings.EnableNonUtf8,
"If 'EnableNonUtf8' is false we shouldn't require an encoding. It's always Utf-8.");
/* This is mucking up the profiler for some reason.
Debug.Assert((type == EncodingType.Primary) || (type == EncodingType.Secondary),
"Unknown 'EncodingType' value: " + type.ToString());
*/
if (((type == EncodingType.Primary) && (!HttpSysSettings.FavorUtf8)) ||
((type == EncodingType.Secondary) && (HttpSysSettings.FavorUtf8)))
{
return AnsiEncoding;
}
else
{
return Utf8Encoding;
}
}
private ParsingResult BuildRequestUriUsingRawPath(Encoding encoding)
{
Debug.Assert(encoding != null, "'encoding' must be assigned.");
Debug.Assert(!string.IsNullOrEmpty(_rawPath), "'rawPath' must have at least one character.");
_rawOctets = new List<byte>();
_requestUriString = new StringBuilder();
_requestUriString.Append(_cookedUriScheme);
_requestUriString.Append(Constants.SchemeDelimiter);
_requestUriString.Append(_cookedUriHost);
ParsingResult result = ParseRawPath(encoding);
if (result == ParsingResult.Success)
{
_requestUriString.Append(_cookedUriQuery);
Debug.Assert(_rawOctets.Count == 0,
"Still raw octets left. They must be added to the result path.");
if (!Uri.TryCreate(_requestUriString.ToString(), UriKind.Absolute, out _requestUri))
{
// If we can't create a Uri from the string, this is an invalid string and it doesn't make
// sense to try another encoding.
result = ParsingResult.InvalidString;
}
}
if (result != ParsingResult.Success)
{
LogWarning("BuildRequestUriUsingRawPath", "Can't convert the raw path: " + _rawPath + " Encoding: " + encoding.WebName);
}
return result;
}
private ParsingResult ParseRawPath(Encoding encoding)
{
Debug.Assert(encoding != null, "'encoding' must be assigned.");
@ -323,45 +121,31 @@ namespace Microsoft.Net.Http.Server
if (current == '%')
{
// Assert is enough, since http.sys accepted the request string already. This should never happen.
Debug.Assert(index + 2 < _rawPath.Length, "Expected >=2 characters after '%' (e.g. %2F)");
Debug.Assert(index + 2 < _rawPath.Length, "Expected at least 2 characters after '%' (e.g. %20)");
index++;
current = _rawPath[index];
if (current == 'u' || current == 'U')
{
// We found "%u" which means, we have a Unicode code point of the form "%uXXXX".
Debug.Assert(index + 4 < _rawPath.Length, "Expected >=4 characters after '%u' (e.g. %u0062)");
// We have a percent encoded octet: %XX
var octetString = _rawPath.Substring(index + 1, 2);
// Decode the content of rawOctets into percent encoded UTF-8 characters and append them
// to requestUriString.
if (!EmptyDecodeAndAppendRawOctetsList(encoding))
{
return ParsingResult.EncodingError;
}
if (!AppendUnicodeCodePointValuePercentEncoded(_rawPath.Substring(index + 1, 4)))
{
return ParsingResult.InvalidString;
}
index += 5;
}
else
// Leave %2F as is, otherwise add to raw octets list for unescaping
if (octetString == "2F" || octetString == "2f")
{
// We found '%', but not followed by 'u', i.e. we have a percent encoded octed: %XX
if (!AddPercentEncodedOctetToRawOctetsList(encoding, _rawPath.Substring(index, 2)))
{
return ParsingResult.InvalidString;
}
index += 2;
_requestUriString.Append('%');
_requestUriString.Append(octetString);
}
else if (!AddPercentEncodedOctetToRawOctetsList(encoding, octetString))
{
return ParsingResult.InvalidString;
}
index += 3;
}
else
{
// We found a non-'%' character: decode the content of rawOctets into percent encoded
// UTF-8 characters and append it to the result.
if (!EmptyDecodeAndAppendRawOctetsList(encoding))
if (!EmptyDecodeAndAppendDecodedOctetsList(encoding))
{
return ParsingResult.EncodingError;
}
// Append the current character to the result.
_requestUriString.Append(current);
index++;
@ -370,7 +154,7 @@ namespace Microsoft.Net.Http.Server
// if the raw path ends with a sequence of percent encoded octets, make sure those get added to the
// result (requestUriString).
if (!EmptyDecodeAndAppendRawOctetsList(encoding))
if (!EmptyDecodeAndAppendDecodedOctetsList(encoding))
{
return ParsingResult.EncodingError;
}
@ -378,44 +162,12 @@ namespace Microsoft.Net.Http.Server
return ParsingResult.Success;
}
private bool AppendUnicodeCodePointValuePercentEncoded(string codePoint)
{
// http.sys only supports %uXXXX (4 hex-digits), even though unicode code points could have up to
// 6 hex digits. Therefore we parse always 4 characters after %u and convert them to an int.
int codePointValue;
if (!int.TryParse(codePoint, NumberStyles.HexNumber, null, out codePointValue))
{
LogWarning("AppendUnicodeCodePointValuePercentEncoded", "Can't convert code point: " + codePoint);
return false;
}
string unicodeString = null;
try
{
unicodeString = char.ConvertFromUtf32(codePointValue);
AppendOctetsPercentEncoded(_requestUriString, Utf8Encoding.GetBytes(unicodeString));
return true;
}
catch (ArgumentOutOfRangeException)
{
LogWarning("AppendUnicodeCodePointValuePercentEncoded", "Can't convert code point: " + codePoint);
}
catch (EncoderFallbackException e)
{
// If utf8Encoding.GetBytes() fails
LogWarning("AppendUnicodeCodePointValuePercentEncoded", "Can't convert code point: " + unicodeString, e.Message);
}
return false;
}
private bool AddPercentEncodedOctetToRawOctetsList(Encoding encoding, string escapedCharacter)
{
byte encodedValue;
if (!byte.TryParse(escapedCharacter, NumberStyles.HexNumber, null, out encodedValue))
{
LogWarning("AddPercentEncodedOctetToRawOctetsList", "Can't convert code point: " + escapedCharacter);
LogWarning(nameof(AddPercentEncodedOctetToRawOctetsList), "Can't convert code point: " + escapedCharacter);
return false;
}
@ -424,7 +176,7 @@ namespace Microsoft.Net.Http.Server
return true;
}
private bool EmptyDecodeAndAppendRawOctetsList(Encoding encoding)
private bool EmptyDecodeAndAppendDecodedOctetsList(Encoding encoding)
{
if (_rawOctets.Count == 0)
{
@ -436,44 +188,22 @@ namespace Microsoft.Net.Http.Server
{
// If the encoding can get a string out of the byte array, this is a valid string in the
// 'encoding' encoding.
byte[] bytes = _rawOctets.ToArray();
var bytes = _rawOctets.ToArray();
decodedString = encoding.GetString(bytes, 0, bytes.Length);
if (encoding == Utf8Encoding)
{
AppendOctetsPercentEncoded(_requestUriString, bytes);
}
else
{
AppendOctetsPercentEncoded(_requestUriString, Utf8Encoding.GetBytes(decodedString));
}
_requestUriString.Append(decodedString);
_rawOctets.Clear();
return true;
}
catch (DecoderFallbackException e)
{
LogWarning("EmptyDecodeAndAppendRawOctetsList", "Can't convert bytes: " + GetOctetsAsString(_rawOctets), e.Message);
}
catch (EncoderFallbackException e)
{
// If utf8Encoding.GetBytes() fails
LogWarning("EmptyDecodeAndAppendRawOctetsList", "Can't convert bytes: " + decodedString, e.Message);
LogWarning(nameof(EmptyDecodeAndAppendDecodedOctetsList), "Can't convert bytes: " + GetOctetsAsString(_rawOctets), e.Message);
}
return false;
}
private static void AppendOctetsPercentEncoded(StringBuilder target, IEnumerable<byte> octets)
{
foreach (byte octet in octets)
{
target.Append('%');
target.Append(octet.ToString("X2", CultureInfo.InvariantCulture));
}
}
private static string GetOctetsAsString(IEnumerable<byte> octets)
{
StringBuilder octetString = new StringBuilder();

Просмотреть файл

@ -18,6 +18,7 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Hosting.Server;
@ -87,6 +88,7 @@ namespace Microsoft.AspNet.Server.WebListener
[InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
[InlineData("/base path/", "/base%20path/sub path", "/base path", "/sub path")]
[InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
[InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")]
public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
{
string root;
@ -121,6 +123,23 @@ namespace Microsoft.AspNet.Server.WebListener
}
}
[Fact]
public async Task Request_DoubleEscapingAllowed()
{
string root;
using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext =>
{
var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
Assert.Equal("/%2F", requestInfo.Path);
return Task.FromResult(0);
}))
{
var response = await SendSocketRequestAsync(root, "/%252F");
var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
Assert.Equal("200", responseStatusCode);
}
}
[Theory]
// The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
[InlineData("/", "", "/")]
@ -189,5 +208,27 @@ namespace Microsoft.AspNet.Server.WebListener
return await client.GetStringAsync(uri);
}
}
private async Task<string> SendSocketRequestAsync(string address, string path)
{
var uri = new Uri(address);
StringBuilder builder = new StringBuilder();
builder.AppendLine("GET " + path + " HTTP/1.1");
builder.AppendLine("Connection: close");
builder.Append("HOST: ");
builder.AppendLine(uri.Authority);
builder.AppendLine();
byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp))
{
socket.Connect(uri.Host, uri.Port);
socket.Send(request);
var response = new byte[12];
await Task.Run(() => socket.Receive(response));
return Encoding.ASCII.GetString(response);
}
}
}
}

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Xunit;
@ -54,8 +55,9 @@ namespace Microsoft.Net.Http.Server
[InlineData("/basepath/", "/basepath", "/basepath", "")]
[InlineData("/basepath/", "/basepath/", "/basepath", "/")]
[InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
[InlineData("/base path/", "/base%20path/sub path", "/base path", "/sub path")]
[InlineData("/base path/", "/base%20path/sub%20path", "/base path", "/sub path")]
[InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
[InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")]
public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
{
string root;
@ -80,6 +82,36 @@ namespace Microsoft.Net.Http.Server
}
}
[Theory]
[InlineData("/path%")]
[InlineData("/path%XY")]
[InlineData("/path%F")]
[InlineData("/path with spaces")]
public async Task Request_MalformedPathReturns400StatusCode(string requestPath)
{
string root;
using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
{
var responseTask = SendSocketRequestAsync(root, requestPath);
var contextTask = server.GetContextAsync();
var response = await responseTask;
var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
Assert.Equal("400", responseStatusCode);
}
}
[Fact]
public async Task Request_DoubleEscapingAllowed()
{
string root;
using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
{
var responseTask = SendSocketRequestAsync(root, "/%252F");
var context = await server.GetContextAsync();
Assert.Equal("/%2F", context.Request.Path);
}
}
[Theory]
// The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
[InlineData("/", "", "/")]
@ -131,5 +163,27 @@ namespace Microsoft.Net.Http.Server
return await client.GetStringAsync(uri);
}
}
private async Task<string> SendSocketRequestAsync(string address, string path)
{
var uri = new Uri(address);
StringBuilder builder = new StringBuilder();
builder.AppendLine("GET " + path + " HTTP/1.1");
builder.AppendLine("Connection: close");
builder.Append("HOST: ");
builder.AppendLine(uri.Authority);
builder.AppendLine();
byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp))
{
socket.Connect(uri.Host, uri.Port);
socket.Send(request);
var response = new byte[12];
await Task.Run(() => socket.Receive(response));
return Encoding.ASCII.GetString(response);
}
}
}
}