From 0332ed11789f86bc02759d4b30f6c43f863fec47 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 15 Sep 2026 20:22:54 +0200 Subject: [PATCH] Fix case-insensitive TAR extraction on Windows --- .../src/System/Formats/Tar/TarEntry.cs | 16 +++++----------- ...TarFile.ExtractToDirectory.Stream.Tests.cs | 19 +++++++++++++++++++ ...le.ExtractToDirectoryAsync.Stream.Tests.cs | 19 +++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 71035c6a2d440e..617b49b10df210 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -422,12 +422,6 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b // already present on disk before extraction private static bool FilePathEscapesDirectory(string destinationDirectoryPath, string fileDestinationPath) { - // Windows is case insensitive while Linux is case sensitive - // This ensures the comparison is consistent with how the OS would resolve the paths - StringComparison pathComparison = OperatingSystem.IsWindows() - ? StringComparison.OrdinalIgnoreCase - : StringComparison.Ordinal; - string resolvedDest = ResolvePhysicalPath(destinationDirectoryPath); // Use the logical destination path for computing the relative path @@ -444,8 +438,8 @@ private static bool FilePathEscapesDirectory(string destinationDirectoryPath, st string normalizedFile = Path.GetFullPath(fileDestinationPath); // Guard with StartsWith before computing relative path - if (!normalizedFile.StartsWith(logicalPrefix, pathComparison) && - !normalizedFile.Equals(logicalDest, pathComparison)) + if (!normalizedFile.StartsWith(logicalPrefix, StringComparison.Ordinal) && + !normalizedFile.Equals(logicalDest, StringComparison.Ordinal)) { return true; } @@ -465,8 +459,8 @@ private static bool FilePathEscapesDirectory(string destinationDirectoryPath, st current = ResolveSymlink(current); string normalizedCurrent = Path.GetFullPath(current); - if (!normalizedCurrent.StartsWith(destPrefix, pathComparison) && - !normalizedCurrent.Equals(resolvedDest, pathComparison)) + if (!normalizedCurrent.StartsWith(destPrefix, StringComparison.Ordinal) && + !normalizedCurrent.Equals(resolvedDest, StringComparison.Ordinal)) { return true; } @@ -525,7 +519,7 @@ private static string ResolvePhysicalPath(string path) string fullPath = Path.GetFullPath(qualifiedPath); // Removes relative segments - return fullPath.StartsWith(destinationDirectoryFullPath, PathInternal.StringComparison) ? fullPath : null; + return fullPath.StartsWith(destinationDirectoryFullPath, StringComparison.Ordinal) ? fullPath : null; } // Extracts the current entry into the filesystem, regardless of the entry type. diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs index 48079f6b026616..2a376ad7fe7248 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs @@ -74,6 +74,25 @@ public void ExtractEntry_ManySubfolderSegments_NoPrecedingDirectoryEntries() Assert.True(File.Exists(Path.Join(root.Path, fileWithTwoSegments))); } + [Fact] + public void ExtractToDirectory_DifferentlyCasedSiblingDirectory_Throws() + { + using TempDirectory root = new TempDirectory(); + string destinationPath = Path.Join(root.Path, "Dest"); + Directory.CreateDirectory(destinationPath); + + using MemoryStream archive = new MemoryStream(); + using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Pax, leaveOpen: true)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, "../dest/pwn.txt")); + } + + archive.Position = 0; + + Assert.Throws(() => TarFile.ExtractToDirectory(archive, destinationPath, overwriteFiles: false)); + Assert.False(File.Exists(Path.Join(root.Path, "dest", "pwn.txt"))); + } + [Theory] [InlineData(TarEntryType.SymbolicLink)] [InlineData(TarEntryType.HardLink)] diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs index d56f85648914df..fea990d0983d74 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs @@ -97,6 +97,25 @@ public async Task ExtractEntry_ManySubfolderSegments_NoPrecedingDirectoryEntries } } + [Fact] + public async Task ExtractToDirectory_DifferentlyCasedSiblingDirectory_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + string destinationPath = Path.Join(root.Path, "Dest"); + Directory.CreateDirectory(destinationPath); + + await using MemoryStream archive = new MemoryStream(); + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Pax, leaveOpen: true)) + { + await writer.WriteEntryAsync(new PaxTarEntry(TarEntryType.RegularFile, "../dest/pwn.txt")); + } + + archive.Position = 0; + + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationPath, overwriteFiles: false)); + Assert.False(File.Exists(Path.Join(root.Path, "dest", "pwn.txt"))); + } + [Fact] public async Task ExtractEntry_DockerImageTarWithFileTypeInDirectoriesInMode_SuccessfullyExtracts_Async() {