Skip to content

Add detection of Windows reserved filenames #891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
38 changes: 37 additions & 1 deletion src/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using ICSharpCode.SharpZipLib.Core;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace ICSharpCode.SharpZipLib.Zip
Expand Down Expand Up @@ -141,6 +140,10 @@ public string TransformFile(string name)
pathBase += Path.DirectorySeparatorChar;
}

// https://github.com/dotnet/runtime/issues/78834
// For older Windows versions, Path.GetFullPath will alter paths with reserved names to escape them
// e.g. Path.GetFullPath("COM3") gives "\\.\COM3"
// This is not the case for newer versions of Windows, so we need to check for this ourselves
if (!_allowParentTraversal && !Path.GetFullPath(name).StartsWith(pathBase, StringComparison.InvariantCultureIgnoreCase))
{
throw new InvalidNameException("Parent traversal in paths is not allowed");
Expand Down Expand Up @@ -228,6 +231,11 @@ public static string MakeValidName(string name, char replacement)
name = builder.ToString();
}

if (IsReservedName(name))
{
throw new InvalidNameException($"\"{name}\" is a Windows reserved filename. See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions for more information.");
}

// Check for names greater than MaxPath characters.
// TODO: Were is CLR version of MaxPath defined? Can't find it in Environment.
if (name.Length > MaxPath)
Expand Down Expand Up @@ -262,5 +270,33 @@ public char Replacement
_replacementChar = value;
}
}


/// <summary>
/// Checks if the given name is a reserved name in Windows.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>True if the name is reserved; otherwise, false.</returns>
/// <remarks>
/// <para>Microsoft changed the OS behavior for legacy DOS device names in recent versions of Windows.</para>
/// <para>In older versions of Windows, the file extension would be considered for checking if the path is a legacy DOS device name:</para>
/// <list type="bullet">
/// <item><description>Path.GetFullPath("COM3") gives \\.\COM3</description></item>
/// <item><description>Path.GetFullPath("COM3.txt") gives \\.\COM3.txt</description></item>
/// <item><description>Path.GetFullPath("C:\COM3") gives \\.\COM3</description></item>
/// </list>
/// <para>In newer versions of Windows, the file extension is ignored for checking if the path is a legacy DOS device name:</para>
/// <list type="bullet">
/// <item><description>Path.GetFullPath("COM3") gives \\.\COM3</description></item>
/// <item><description>Path.GetFullPath("COM3.txt") gives COM3.txt</description></item>
/// <item><description>Path.GetFullPath("C:\COM3") gives C:\COM3</description></item>
/// </list>
/// <para>Therefore, we can detect if the path is a legacy DOS device name by checking if the full path starts with \\.\ or \\?\.</para>
/// </remarks>
internal static bool IsReservedName(string name)
{
var fullPathName = Path.GetFullPath(name);
return fullPathName.StartsWith(@"\\.\", StringComparison.Ordinal) || fullPathName.StartsWith(@"\\?\", StringComparison.Ordinal);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ public void NameTooLong()
}
}

[Test]
public void TransformDirectory_NameIsReservedName_ThrowsInvalidNameException()
{
var wnt = new WindowsNameTransform();
const string Reserved = "COM1";
var e = Assert.Throws<InvalidNameException>(() => wnt.TransformDirectory(Reserved));

// This second assert is to differentiate between the reserved name exception and the parent traversal in paths not allowed exception
Assert.That(e.Message, Does.Contain("https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions"));
}

[Test]
public void LengthBoundaryOk()
{
Expand Down