Skip to content

Commit

Permalink
Remove unsafe from PathNormalizer (dotnet#56805)
Browse files Browse the repository at this point in the history
  • Loading branch information
ladeak authored Aug 5, 2024
1 parent b41e166 commit b5a97c4
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 361 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ public async Task Caching_SendFileNoContentLength_NotCached()
}
}

[QuarantinedTest("new issue")]
[ConditionalFact]
public async Task Caching_SendFileWithFullContentLength_Cached()
{
Expand Down
260 changes: 90 additions & 170 deletions src/Servers/Kestrel/Core/src/Internal/Http/PathNormalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
Expand Down Expand Up @@ -50,193 +51,112 @@ public static string DecodePath(Span<byte> path, bool pathEncoded, string rawTar
}

// In-place implementation of the algorithm from https://tools.ietf.org/html/rfc3986#section-5.2.4
public static unsafe int RemoveDotSegments(Span<byte> input)
public static int RemoveDotSegments(Span<byte> src)
{
fixed (byte* start = input)
{
var end = start + input.Length;
return RemoveDotSegments(start, end);
}
}

public static unsafe int RemoveDotSegments(byte* start, byte* end)
{
if (!ContainsDotSegments(start, end))
{
return (int)(end - start);
}
Debug.Assert(src[0] == '/', "Path segment must always start with a '/'");
ReadOnlySpan<byte> dotSlash = "./"u8;
ReadOnlySpan<byte> slashDot = "/."u8;

var src = start;
var dst = start;
var writtenLength = 0;
var readPointer = 0;

while (src < end)
while (src.Length > readPointer)
{
var ch1 = *src;
Debug.Assert(ch1 == '/', "Path segment must always start with a '/'");

byte ch2, ch3, ch4;

switch (end - src)
var currentSrc = src[readPointer..];
var nextDotSegmentIndex = currentSrc.IndexOf(slashDot);
if (nextDotSegmentIndex < 0 && readPointer == 0)
{
case 1:
break;
case 2:
ch2 = *(src + 1);

if (ch2 == ByteDot)
{
// B. if the input buffer begins with a prefix of "/./" or "/.",
// where "." is a complete path segment, then replace that
// prefix with "/" in the input buffer; otherwise,
src += 1;
*src = ByteSlash;
continue;
}

break;
case 3:
ch2 = *(src + 1);
ch3 = *(src + 2);

if (ch2 == ByteDot && ch3 == ByteDot)
{
// C. if the input buffer begins with a prefix of "/../" or "/..",
// where ".." is a complete path segment, then replace that
// prefix with "/" in the input buffer and remove the last
// segment and its preceding "/" (if any) from the output
// buffer; otherwise,
src += 2;
*src = ByteSlash;

if (dst > start)
{
do
{
dst--;
} while (dst > start && *dst != ByteSlash);
}

continue;
}
else if (ch2 == ByteDot && ch3 == ByteSlash)
{
// B. if the input buffer begins with a prefix of "/./" or "/.",
// where "." is a complete path segment, then replace that
// prefix with "/" in the input buffer; otherwise,
src += 2;
continue;
}

break;
default:
ch2 = *(src + 1);
ch3 = *(src + 2);
ch4 = *(src + 3);

if (ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash)
{
// C. if the input buffer begins with a prefix of "/../" or "/..",
// where ".." is a complete path segment, then replace that
// prefix with "/" in the input buffer and remove the last
// segment and its preceding "/" (if any) from the output
// buffer; otherwise,
src += 3;

if (dst > start)
{
do
{
dst--;
} while (dst > start && *dst != ByteSlash);
}

continue;
}
else if (ch2 == ByteDot && ch3 == ByteSlash)
{
// B. if the input buffer begins with a prefix of "/./" or "/.",
// where "." is a complete path segment, then replace that
// prefix with "/" in the input buffer; otherwise,
src += 2;
continue;
}

break;
return src.Length;
}

// E. move the first path segment in the input buffer to the end of
// the output buffer, including the initial "/" character (if
// any) and any subsequent characters up to, but not including,
// the next "/" character or the end of the input buffer.
do
if (nextDotSegmentIndex < 0)
{
*dst++ = ch1;
ch1 = *++src;
} while (src < end && ch1 != ByteSlash);
}

if (dst == start)
{
*dst++ = ByteSlash;
}

return (int)(dst - start);
}

public static unsafe bool ContainsDotSegments(byte* start, byte* end)
{
var src = start;
while (src < end)
{
var ch1 = *src;
Debug.Assert(ch1 == '/', "Path segment must always start with a '/'");

byte ch2, ch3, ch4;

switch (end - src)
// Copy the remianing src to dst, and return.
currentSrc.CopyTo(src[writtenLength..]);
writtenLength += src.Length - readPointer;
return writtenLength;
}
else if (nextDotSegmentIndex > 0)
{
case 1:
break;
case 2:
ch2 = *(src + 1);
// Copy until the next segment excluding the trailer.
currentSrc[..nextDotSegmentIndex].CopyTo(src[writtenLength..]);
writtenLength += nextDotSegmentIndex;
readPointer += nextDotSegmentIndex;
}

if (ch2 == ByteDot)
{
return true;
}
var remainingLength = src.Length - readPointer;

break;
case 3:
ch2 = *(src + 1);
ch3 = *(src + 2);
// Case of /../ or /./ or non-dot segments.
if (remainingLength > 3)
{
var nextIndex = readPointer + 2;

if (src[nextIndex] == ByteSlash)
{
// Case: /./
readPointer = nextIndex;
}
else if (MemoryMarshal.CreateSpan(ref src[nextIndex], 2).StartsWith(dotSlash))
{
// Case: /../
// Remove the last segment and replace the path with /
var lastIndex = MemoryMarshal.CreateSpan(ref src[0], writtenLength).LastIndexOf(ByteSlash);

// Move write pointer to the end of the previous segment without / or to start position
writtenLength = int.Max(0, lastIndex);

// Move the read pointer to the next segments beginning including /
readPointer += 3;
}
else
{
// Not a dot segment e.g. /.a, copy the matched /. and the next character then bump the read pointer
src.Slice(readPointer, 3).CopyTo(src[writtenLength..]);
writtenLength += 3;
readPointer = nextIndex + 1;
}
}

if ((ch2 == ByteDot && ch3 == ByteDot) ||
(ch2 == ByteDot && ch3 == ByteSlash))
// Ending with /.. or /./ or non-dot segments.
else if (remainingLength == 3)
{
var nextIndex = readPointer + 2;
if (src[nextIndex] == ByteSlash)
{
// Case: /./ Replace the /./ segment with a closing /
src[writtenLength++] = ByteSlash;
return writtenLength;
}
else if (src[nextIndex] == ByteDot)
{
// Case: /.. Remove the last segment and replace the path with /
var lastSlashIndex = MemoryMarshal.CreateSpan(ref src[0], writtenLength).LastIndexOf(ByteSlash);

// If this was the beginning of the string, then return /
if (lastSlashIndex < 0)
{
return true;
Debug.Assert(src[0] == '/');
return 1;
}

break;
default:
ch2 = *(src + 1);
ch3 = *(src + 2);
ch4 = *(src + 3);

if ((ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash) ||
(ch2 == ByteDot && ch3 == ByteSlash))
else
{
return true;
writtenLength = lastSlashIndex + 1;
}

break;
return writtenLength;
}
else
{
// Not a dot segment e.g. /.a, copy the remaining part.
src[readPointer..].CopyTo(src[writtenLength..]);
return writtenLength + 3;
}
}

do
// Ending with /.
else if (remainingLength == 2)
{
ch1 = *++src;
} while (src < end && ch1 != ByteSlash);
src[writtenLength++] = ByteSlash;
return writtenLength;
}
}

return false;
return writtenLength;
}
}
25 changes: 24 additions & 1 deletion src/Servers/Kestrel/Core/test/PathNormalizerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
Expand Down Expand Up @@ -54,6 +54,29 @@ public class PathNormalizerTests
[InlineData("/", "/")]
[InlineData("/no/segments", "/no/segments")]
[InlineData("/no/segments/", "/no/segments/")]
[InlineData("/././", "/")]
[InlineData("/./.", "/")]
[InlineData("/../..", "/")]
[InlineData("/../../", "/")]
[InlineData("/../.", "/")]
[InlineData("/./..", "/")]
[InlineData("/.././", "/")]
[InlineData("/./../", "/")]
[InlineData("/..", "/")]
[InlineData("/.", "/")]
[InlineData("/a/abc/../abc/../b", "/a/b")]
[InlineData("/a/abc/.a", "/a/abc/.a")]
[InlineData("/a/abc/..a", "/a/abc/..a")]
[InlineData("/a/.b/c", "/a/.b/c")]
[InlineData("/a/.b/../c", "/a/c")]
[InlineData("/a/../.b/./c", "/.b/c")]
[InlineData("/a/.b/./c", "/a/.b/c")]
[InlineData("/a/./.b/./c", "/a/.b/c")]
[InlineData("/a/..b/c", "/a/..b/c")]
[InlineData("/a/..b/../c", "/a/c")]
[InlineData("/a/../..b/./c", "/..b/c")]
[InlineData("/a/..b/./c", "/a/..b/c")]
[InlineData("/a/./..b/./c", "/a/..b/c")]
public void RemovesDotSegments(string input, string expected)
{
var data = Encoding.ASCII.GetBytes(input);
Expand Down
Loading

0 comments on commit b5a97c4

Please sign in to comment.