Normalize request path to NFC and resolve dot segments (#273).

This commit is contained in:
Cesar Blum Silveira 2016-01-08 15:41:53 -08:00
parent d616f0ccb0
commit 1209eca3fa
9 changed files with 276 additions and 4 deletions

View File

@ -741,6 +741,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
// URI was encoded, unescape and then parse as utf8
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
requestUrlPath = pathBegin.GetUtf8String(pathEnd);
requestUrlPath = PathNormalizer.NormalizeToNFC(requestUrlPath);
}
else
{
@ -748,6 +749,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
requestUrlPath = pathBegin.GetAsciiString(pathEnd);
}
requestUrlPath = PathNormalizer.RemoveDotSegments(requestUrlPath);
consumed = scan;
Method = method;
RequestUri = requestUrlPath;

View File

@ -0,0 +1,132 @@
// 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.Buffers;
using System.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Http
{
public static class PathNormalizer
{
public static string NormalizeToNFC(string path)
{
if (!path.IsNormalized(NormalizationForm.FormC))
{
path = path.Normalize(NormalizationForm.FormC);
}
return path;
}
public static string RemoveDotSegments(string path)
{
if (ContainsDotSegments(path))
{
var normalizedChars = ArrayPool<char>.Shared.Rent(path.Length);
var normalizedIndex = normalizedChars.Length;
var pathIndex = path.Length - 1;
var skipSegments = 0;
while (pathIndex >= 0)
{
if (pathIndex >= 2 && path[pathIndex] == '.' && path[pathIndex - 1] == '.' && path[pathIndex - 2] == '/')
{
if (normalizedIndex == normalizedChars.Length || normalizedChars[normalizedIndex] != '/')
{
normalizedChars[--normalizedIndex] = '/';
}
skipSegments++;
pathIndex -= 3;
}
else if (pathIndex >= 1 && path[pathIndex] == '.' && path[pathIndex - 1] == '/')
{
pathIndex -= 2;
}
else
{
while (pathIndex >= 0)
{
var lastChar = path[pathIndex];
if (skipSegments == 0)
{
normalizedChars[--normalizedIndex] = lastChar;
}
pathIndex--;
if (lastChar == '/')
{
break;
}
}
if (skipSegments > 0)
{
skipSegments--;
}
}
}
path = new string(normalizedChars, normalizedIndex, normalizedChars.Length - normalizedIndex);
ArrayPool<char>.Shared.Return(normalizedChars);
}
return path;
}
private unsafe static bool ContainsDotSegments(string path)
{
fixed (char* ptr = path)
{
char* end = ptr + path.Length;
for (char* p = ptr; p < end; p++)
{
if (*p == '/')
{
p++;
}
if (p == end)
{
return false;
}
if (*p == '.')
{
p++;
if (p == end)
{
return true;
}
if (*p == '.')
{
p++;
if (p == end)
{
return true;
}
if (*p == '/')
{
return true;
}
}
else if (*p == '/')
{
return true;
}
}
}
}
return false;
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Globalization;
using Microsoft.AspNetCore.Server.Kestrel.Http;
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel
@ -147,6 +148,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel
serverAddress.PathBase = url.Substring(pathDelimiterEnd);
}
serverAddress.PathBase = PathNormalizer.NormalizeToNFC(serverAddress.PathBase);
return serverAddress;
}
}

View File

@ -6,6 +6,7 @@
"url": "git://github.com/aspnet/kestrelhttpserver"
},
"dependencies": {
"System.Buffers": "4.0.0-*",
"Microsoft.AspNetCore.Hosting": "1.0.0-*",
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.PlatformAbstractions": "1.0.0-*",

View File

@ -3,7 +3,10 @@
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -88,7 +91,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
[IPv6SupportedCondition]
public Task RemoteIPv6Address()
{
return TestRemoteIPAddress("[::1]", "[::1]", "::1", "8792");
return TestRemoteIPAddress("[::1]", "[::1]", "::1", "8793");
}
[ConditionalFact]
@ -97,7 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
var config = new ConfigurationBuilder().AddInMemoryCollection(
new Dictionary<string, string> {
{ "server.urls", "http://localhost:8791" }
{ "server.urls", "http://localhost:8794" }
}).Build();
var builder = new WebHostBuilder()
@ -120,11 +123,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
client.DefaultRequestHeaders.Connection.Clear();
client.DefaultRequestHeaders.Connection.Add("close");
var response = await client.GetAsync("http://localhost:8791/");
var response = await client.GetAsync("http://localhost:8794/");
response.EnsureSuccessStatusCode();
}
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")]
public async Task RequestPathIsNormalized()
{
var config = new ConfigurationBuilder().AddInMemoryCollection(
new Dictionary<string, string> {
{ "server.urls", "http://localhost:8795/\u0041\u030A" }
}).Build();
var builder = new WebHostBuilder()
.UseConfiguration(config)
.UseServer("Microsoft.AspNetCore.Server.Kestrel")
.Configure(app =>
{
app.Run(async context =>
{
var connection = context.Connection;
Assert.Equal("/\u00C5", context.Request.PathBase.Value);
Assert.Equal("/B/\u00C5", context.Request.Path.Value);
await context.Response.WriteAsync("hello, world");
});
});
using (var host = builder.Build())
{
host.Start();
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
socket.Connect(new IPEndPoint(IPAddress.Loopback, 8795));
socket.Send(Encoding.ASCII.GetBytes("GET /%41%CC%8A/A/../B/%41%CC%8A HTTP/1.1\r\n\r\n"));
socket.Shutdown(SocketShutdown.Send);
var response = new StringBuilder();
var buffer = new byte[4096];
while (true)
{
var length = socket.Receive(buffer);
if (length == 0)
{
break;
}
response.Append(Encoding.ASCII.GetString(buffer, 0, length));
}
Assert.StartsWith("HTTP/1.1 200 OK", response.ToString());
}
}
}
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress, string port)
{
var config = new ConfigurationBuilder().AddInMemoryCollection(

View File

@ -1120,7 +1120,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
try
{
// Ensure write is long enough to disable write-behind buffering
for (int i = 0; i < 10; i++)
for (int i = 0; i < 100; i++)
{
await response.WriteAsync(largeString, lifetime.RequestAborted);
}

View File

@ -0,0 +1,63 @@
// 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.Text;
using Microsoft.AspNetCore.Server.Kestrel.Http;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class PathNormalizerTests
{
[Theory]
[InlineData("/a", "/a")]
[InlineData("/a/", "/a/")]
[InlineData("/a/b", "/a/b")]
[InlineData("/a/b/", "/a/b/")]
[InlineData("/a", "/./a")]
[InlineData("/a", "/././a")]
[InlineData("/a", "/../a")]
[InlineData("/a", "/../../a")]
[InlineData("/a/b", "/a/./b")]
[InlineData("/b", "/a/../b")]
[InlineData("/a/", "/a/./")]
[InlineData("/a", "/a/.")]
[InlineData("/", "/a/../b/../")]
[InlineData("/", "/a/../b/..")]
[InlineData("/b", "/a/../../b")]
[InlineData("/b/", "/a/../../b/")]
[InlineData("/b", "/a/.././../b")]
[InlineData("/b/", "/a/.././../b/")]
[InlineData("/a/d", "/a/b/c/./../../d")]
[InlineData("/a/d", "/./a/b/c/./../../d")]
[InlineData("/a/d", "/../a/b/c/./../../d")]
[InlineData("/a/d", "/./../a/b/c/./../../d")]
[InlineData("/a/d", "/.././a/b/c/./../../d")]
[InlineData("/.a", "/.a")]
[InlineData("/..a", "/..a")]
[InlineData("/...", "/...")]
[InlineData("/a/.../b", "/a/.../b")]
[InlineData("/b", "/a/../.../../b")]
[InlineData("/a/.b", "/a/.b")]
[InlineData("/a/..b", "/a/..b")]
[InlineData("/a/b.", "/a/b.")]
[InlineData("/a/b..", "/a/b..")]
[InlineData("a/b", "a/b")]
[InlineData("a/c", "a/b/../c")]
[InlineData("*", "*")]
public void RemovesDotSegments(string expected, string input)
{
var result = PathNormalizer.RemoveDotSegments(input);
Assert.Equal(expected, result);
}
[Fact]
public void NormalizesToNFC()
{
var result = PathNormalizer.NormalizeToNFC("/\u0041\u030A");
Assert.True(result.IsNormalized(NormalizationForm.FormC));
Assert.Equal("/\u00C5", result);
}
}
}

View File

@ -1,3 +1,8 @@
// 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.Text;
using Microsoft.AspNetCore.Server.Kestrel;
using Xunit;
@ -43,5 +48,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(port, serverAddress.Port);
Assert.Equal(pathBase, serverAddress.PathBase);
}
[Fact]
public void PathBaseIsNormalized()
{
var serverAddres = ServerAddress.FromUrl("http://localhost:8080/p\u0041\u030Athbase");
Assert.True(serverAddres.PathBase.IsNormalized(NormalizationForm.FormC));
Assert.Equal("/p\u00C5thbase", serverAddres.PathBase);
}
}
}

View File

@ -12,6 +12,8 @@
"dnxcore50": {
"dependencies": {
"System.Diagnostics.TraceSource": "4.0.0-*",
"System.Globalization.Extensions": "4.0.1-*",
"System.IO": "4.1.0-*",
"System.Net.Http.WinHttpHandler": "4.0.0-*",
"System.Net.Sockets": "4.1.0-*",
"System.Runtime.Handles": "4.0.1-*"