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

Commit 1209eca

Browse files
author
Cesar Blum Silveira
committed
Normalize request path to NFC and resolve dot segments (#273).
1 parent d616f0c commit 1209eca

File tree

9 files changed

+276
-4
lines changed

9 files changed

+276
-4
lines changed

src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,13 +741,16 @@ protected bool TakeStartLine(SocketInput input)
741741
// URI was encoded, unescape and then parse as utf8
742742
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
743743
requestUrlPath = pathBegin.GetUtf8String(pathEnd);
744+
requestUrlPath = PathNormalizer.NormalizeToNFC(requestUrlPath);
744745
}
745746
else
746747
{
747748
// URI wasn't encoded, parse as ASCII
748749
requestUrlPath = pathBegin.GetAsciiString(pathEnd);
749750
}
750751

752+
requestUrlPath = PathNormalizer.RemoveDotSegments(requestUrlPath);
753+
751754
consumed = scan;
752755
Method = method;
753756
RequestUri = requestUrlPath;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Text;
7+
8+
namespace Microsoft.AspNetCore.Server.Kestrel.Http
9+
{
10+
public static class PathNormalizer
11+
{
12+
public static string NormalizeToNFC(string path)
13+
{
14+
if (!path.IsNormalized(NormalizationForm.FormC))
15+
{
16+
path = path.Normalize(NormalizationForm.FormC);
17+
}
18+
19+
return path;
20+
}
21+
22+
public static string RemoveDotSegments(string path)
23+
{
24+
if (ContainsDotSegments(path))
25+
{
26+
var normalizedChars = ArrayPool<char>.Shared.Rent(path.Length);
27+
var normalizedIndex = normalizedChars.Length;
28+
var pathIndex = path.Length - 1;
29+
var skipSegments = 0;
30+
31+
while (pathIndex >= 0)
32+
{
33+
if (pathIndex >= 2 && path[pathIndex] == '.' && path[pathIndex - 1] == '.' && path[pathIndex - 2] == '/')
34+
{
35+
if (normalizedIndex == normalizedChars.Length || normalizedChars[normalizedIndex] != '/')
36+
{
37+
normalizedChars[--normalizedIndex] = '/';
38+
}
39+
40+
skipSegments++;
41+
pathIndex -= 3;
42+
}
43+
else if (pathIndex >= 1 && path[pathIndex] == '.' && path[pathIndex - 1] == '/')
44+
{
45+
pathIndex -= 2;
46+
}
47+
else
48+
{
49+
while (pathIndex >= 0)
50+
{
51+
var lastChar = path[pathIndex];
52+
53+
if (skipSegments == 0)
54+
{
55+
normalizedChars[--normalizedIndex] = lastChar;
56+
}
57+
58+
pathIndex--;
59+
60+
if (lastChar == '/')
61+
{
62+
break;
63+
}
64+
}
65+
66+
if (skipSegments > 0)
67+
{
68+
skipSegments--;
69+
}
70+
}
71+
}
72+
73+
path = new string(normalizedChars, normalizedIndex, normalizedChars.Length - normalizedIndex);
74+
ArrayPool<char>.Shared.Return(normalizedChars);
75+
}
76+
77+
return path;
78+
}
79+
80+
private unsafe static bool ContainsDotSegments(string path)
81+
{
82+
fixed (char* ptr = path)
83+
{
84+
char* end = ptr + path.Length;
85+
86+
for (char* p = ptr; p < end; p++)
87+
{
88+
if (*p == '/')
89+
{
90+
p++;
91+
}
92+
93+
if (p == end)
94+
{
95+
return false;
96+
}
97+
98+
if (*p == '.')
99+
{
100+
p++;
101+
102+
if (p == end)
103+
{
104+
return true;
105+
}
106+
107+
if (*p == '.')
108+
{
109+
p++;
110+
111+
if (p == end)
112+
{
113+
return true;
114+
}
115+
116+
if (*p == '/')
117+
{
118+
return true;
119+
}
120+
}
121+
else if (*p == '/')
122+
{
123+
return true;
124+
}
125+
}
126+
}
127+
}
128+
129+
return false;
130+
}
131+
}
132+
}

src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Diagnostics;
66
using System.Globalization;
7+
using Microsoft.AspNetCore.Server.Kestrel.Http;
78
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
89

910
namespace Microsoft.AspNetCore.Server.Kestrel
@@ -147,6 +148,8 @@ public static ServerAddress FromUrl(string url)
147148
serverAddress.PathBase = url.Substring(pathDelimiterEnd);
148149
}
149150

151+
serverAddress.PathBase = PathNormalizer.NormalizeToNFC(serverAddress.PathBase);
152+
150153
return serverAddress;
151154
}
152155
}

src/Microsoft.AspNetCore.Server.Kestrel/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"url": "git://github.com/aspnet/kestrelhttpserver"
77
},
88
"dependencies": {
9+
"System.Buffers": "4.0.0-*",
910
"Microsoft.AspNetCore.Hosting": "1.0.0-*",
1011
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
1112
"Microsoft.Extensions.PlatformAbstractions": "1.0.0-*",

test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
using System.Collections.Generic;
55
using System.Globalization;
6+
using System.Net;
67
using System.Net.Http;
8+
using System.Net.Sockets;
9+
using System.Text;
710
using System.Threading.Tasks;
811
using Microsoft.AspNetCore.Builder;
912
using Microsoft.AspNetCore.Hosting;
@@ -88,7 +91,7 @@ public Task RemoteIPv4Address(string requestAddress, string expectAddress, strin
8891
[IPv6SupportedCondition]
8992
public Task RemoteIPv6Address()
9093
{
91-
return TestRemoteIPAddress("[::1]", "[::1]", "::1", "8792");
94+
return TestRemoteIPAddress("[::1]", "[::1]", "::1", "8793");
9295
}
9396

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

103106
var builder = new WebHostBuilder()
@@ -120,11 +123,62 @@ public async Task DoesNotHangOnConnectionCloseRequest()
120123
client.DefaultRequestHeaders.Connection.Clear();
121124
client.DefaultRequestHeaders.Connection.Add("close");
122125

123-
var response = await client.GetAsync("http://localhost:8791/");
126+
var response = await client.GetAsync("http://localhost:8794/");
124127
response.EnsureSuccessStatusCode();
125128
}
126129
}
127130

131+
[ConditionalFact]
132+
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")]
133+
public async Task RequestPathIsNormalized()
134+
{
135+
var config = new ConfigurationBuilder().AddInMemoryCollection(
136+
new Dictionary<string, string> {
137+
{ "server.urls", "http://localhost:8795/\u0041\u030A" }
138+
}).Build();
139+
140+
var builder = new WebHostBuilder()
141+
.UseConfiguration(config)
142+
.UseServer("Microsoft.AspNetCore.Server.Kestrel")
143+
.Configure(app =>
144+
{
145+
app.Run(async context =>
146+
{
147+
var connection = context.Connection;
148+
Assert.Equal("/\u00C5", context.Request.PathBase.Value);
149+
Assert.Equal("/B/\u00C5", context.Request.Path.Value);
150+
await context.Response.WriteAsync("hello, world");
151+
});
152+
});
153+
154+
using (var host = builder.Build())
155+
{
156+
host.Start();
157+
158+
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
159+
{
160+
socket.Connect(new IPEndPoint(IPAddress.Loopback, 8795));
161+
socket.Send(Encoding.ASCII.GetBytes("GET /%41%CC%8A/A/../B/%41%CC%8A HTTP/1.1\r\n\r\n"));
162+
socket.Shutdown(SocketShutdown.Send);
163+
164+
var response = new StringBuilder();
165+
var buffer = new byte[4096];
166+
while (true)
167+
{
168+
var length = socket.Receive(buffer);
169+
if (length == 0)
170+
{
171+
break;
172+
}
173+
174+
response.Append(Encoding.ASCII.GetString(buffer, 0, length));
175+
}
176+
177+
Assert.StartsWith("HTTP/1.1 200 OK", response.ToString());
178+
}
179+
}
180+
}
181+
128182
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress, string port)
129183
{
130184
var config = new ConfigurationBuilder().AddInMemoryCollection(

test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ public async Task FailedWritesResultInAbortedRequest(ServiceContext testContext)
11201120
try
11211121
{
11221122
// Ensure write is long enough to disable write-behind buffering
1123-
for (int i = 0; i < 10; i++)
1123+
for (int i = 0; i < 100; i++)
11241124
{
11251125
await response.WriteAsync(largeString, lifetime.RequestAborted);
11261126
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Text;
6+
using Microsoft.AspNetCore.Server.Kestrel.Http;
7+
using Xunit;
8+
9+
namespace Microsoft.AspNetCore.Server.KestrelTests
10+
{
11+
public class PathNormalizerTests
12+
{
13+
[Theory]
14+
[InlineData("/a", "/a")]
15+
[InlineData("/a/", "/a/")]
16+
[InlineData("/a/b", "/a/b")]
17+
[InlineData("/a/b/", "/a/b/")]
18+
[InlineData("/a", "/./a")]
19+
[InlineData("/a", "/././a")]
20+
[InlineData("/a", "/../a")]
21+
[InlineData("/a", "/../../a")]
22+
[InlineData("/a/b", "/a/./b")]
23+
[InlineData("/b", "/a/../b")]
24+
[InlineData("/a/", "/a/./")]
25+
[InlineData("/a", "/a/.")]
26+
[InlineData("/", "/a/../b/../")]
27+
[InlineData("/", "/a/../b/..")]
28+
[InlineData("/b", "/a/../../b")]
29+
[InlineData("/b/", "/a/../../b/")]
30+
[InlineData("/b", "/a/.././../b")]
31+
[InlineData("/b/", "/a/.././../b/")]
32+
[InlineData("/a/d", "/a/b/c/./../../d")]
33+
[InlineData("/a/d", "/./a/b/c/./../../d")]
34+
[InlineData("/a/d", "/../a/b/c/./../../d")]
35+
[InlineData("/a/d", "/./../a/b/c/./../../d")]
36+
[InlineData("/a/d", "/.././a/b/c/./../../d")]
37+
[InlineData("/.a", "/.a")]
38+
[InlineData("/..a", "/..a")]
39+
[InlineData("/...", "/...")]
40+
[InlineData("/a/.../b", "/a/.../b")]
41+
[InlineData("/b", "/a/../.../../b")]
42+
[InlineData("/a/.b", "/a/.b")]
43+
[InlineData("/a/..b", "/a/..b")]
44+
[InlineData("/a/b.", "/a/b.")]
45+
[InlineData("/a/b..", "/a/b..")]
46+
[InlineData("a/b", "a/b")]
47+
[InlineData("a/c", "a/b/../c")]
48+
[InlineData("*", "*")]
49+
public void RemovesDotSegments(string expected, string input)
50+
{
51+
var result = PathNormalizer.RemoveDotSegments(input);
52+
Assert.Equal(expected, result);
53+
}
54+
55+
[Fact]
56+
public void NormalizesToNFC()
57+
{
58+
var result = PathNormalizer.NormalizeToNFC("/\u0041\u030A");
59+
Assert.True(result.IsNormalized(NormalizationForm.FormC));
60+
Assert.Equal("/\u00C5", result);
61+
}
62+
}
63+
}

test/Microsoft.AspNetCore.Server.KestrelTests/ServerAddressFacts.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Text;
16
using Microsoft.AspNetCore.Server.Kestrel;
27
using Xunit;
38

@@ -43,5 +48,14 @@ public void UrlsAreParsedCorrectly(string url, string scheme, string host, int p
4348
Assert.Equal(port, serverAddress.Port);
4449
Assert.Equal(pathBase, serverAddress.PathBase);
4550
}
51+
52+
[Fact]
53+
public void PathBaseIsNormalized()
54+
{
55+
var serverAddres = ServerAddress.FromUrl("http://localhost:8080/p\u0041\u030Athbase");
56+
57+
Assert.True(serverAddres.PathBase.IsNormalized(NormalizationForm.FormC));
58+
Assert.Equal("/p\u00C5thbase", serverAddres.PathBase);
59+
}
4660
}
4761
}

test/Microsoft.AspNetCore.Server.KestrelTests/project.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"dnxcore50": {
1313
"dependencies": {
1414
"System.Diagnostics.TraceSource": "4.0.0-*",
15+
"System.Globalization.Extensions": "4.0.1-*",
16+
"System.IO": "4.1.0-*",
1517
"System.Net.Http.WinHttpHandler": "4.0.0-*",
1618
"System.Net.Sockets": "4.1.0-*",
1719
"System.Runtime.Handles": "4.0.1-*"

0 commit comments

Comments
 (0)