Normalize request path to NFC and resolve dot segments (#273).
This commit is contained in:
parent
d616f0ccb0
commit
1209eca3fa
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-*",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-*"
|
||||
|
|
|
|||
Loading…
Reference in New Issue