Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

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

Merged
merged 1 commit into from
Jan 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -741,13 +741,16 @@ protected bool TakeStartLine(SocketInput input)
// URI was encoded, unescape and then parse as utf8
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
requestUrlPath = pathBegin.GetUtf8String(pathEnd);
requestUrlPath = PathNormalizer.NormalizeToNFC(requestUrlPath);
}
else
{
// URI wasn't encoded, parse as ASCII
requestUrlPath = pathBegin.GetAsciiString(pathEnd);
}

requestUrlPath = PathNormalizer.RemoveDotSegments(requestUrlPath);

consumed = scan;
Method = method;
RequestUri = requestUrlPath;
Expand Down
132 changes: 132 additions & 0 deletions src/Microsoft.AspNetCore.Server.Kestrel/Http/PathNormalizer.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
3 changes: 3 additions & 0 deletions src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -147,6 +148,8 @@ public static ServerAddress FromUrl(string url)
serverAddress.PathBase = url.Substring(pathDelimiterEnd);
}

serverAddress.PathBase = PathNormalizer.NormalizeToNFC(serverAddress.PathBase);

return serverAddress;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.AspNetCore.Server.Kestrel/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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-*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,7 +91,7 @@ public Task RemoteIPv4Address(string requestAddress, string expectAddress, strin
[IPv6SupportedCondition]
public Task RemoteIPv6Address()
{
return TestRemoteIPAddress("[::1]", "[::1]", "::1", "8792");
return TestRemoteIPAddress("[::1]", "[::1]", "::1", "8793");
}

[ConditionalFact]
Expand All @@ -97,7 +100,7 @@ public async Task DoesNotHangOnConnectionCloseRequest()
{
var config = new ConfigurationBuilder().AddInMemoryCollection(
new Dictionary<string, string> {
{ "server.urls", "http://localhost:8791" }
{ "server.urls", "http://localhost:8794" }
}).Build();

var builder = new WebHostBuilder()
Expand All @@ -120,11 +123,62 @@ public async Task DoesNotHangOnConnectionCloseRequest()
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,7 @@ public async Task FailedWritesResultInAbortedRequest(ServiceContext testContext)
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -43,5 +48,14 @@ public void UrlsAreParsedCorrectly(string url, string scheme, string host, int p
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);
}
}
}
2 changes: 2 additions & 0 deletions test/Microsoft.AspNetCore.Server.KestrelTests/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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-*"
Expand Down