diff --git a/Projects/UOContent.Tests/Tests/WorldSaves/ManagedArchiveTests.cs b/Projects/UOContent.Tests/Tests/WorldSaves/ManagedArchiveTests.cs index b5a2c0dc9..7103a65c5 100644 --- a/Projects/UOContent.Tests/Tests/WorldSaves/ManagedArchiveTests.cs +++ b/Projects/UOContent.Tests/Tests/WorldSaves/ManagedArchiveTests.cs @@ -311,6 +311,50 @@ public class ManagedArchiveTests : IDisposable Assert.Equal("Plain tar test data", File.ReadAllText(Path.Combine(extractDir, "plain.txt"))); } + [Theory] + [InlineData("../escaped.txt")] + [InlineData("sub/../../escaped.txt")] + public void ExtractTar_EntryOutsideOutputDirectory_IsRejected(string entryName) + { + var archivePath = Path.Combine(_testDir, "traversal.tar"); + + using (var fileStream = new FileStream(archivePath, FileMode.Create)) + using (var tarWriter = new TarWriter(fileStream, TarEntryFormat.Pax, leaveOpen: true)) + { + var entry = new PaxTarEntry(TarEntryType.RegularFile, entryName) + { + DataStream = new MemoryStream("escaped"u8.ToArray()) + }; + tarWriter.WriteEntry(entry); + } + + var extractDir = Path.Combine(_testDir, "out", "extract"); + + Assert.False(ManagedArchive.ExtractTar(archivePath, extractDir)); + Assert.False(File.Exists(Path.Combine(_testDir, "out", "escaped.txt"))); + Assert.False(File.Exists(Path.Combine(_testDir, "escaped.txt"))); + } + + [Fact] + public void ExtractTar_RootedEntry_IsRejected() + { + var archivePath = Path.Combine(_testDir, "rooted.tar"); + var target = Path.Combine(_testDir, "rooted-escaped.txt"); + + using (var fileStream = new FileStream(archivePath, FileMode.Create)) + using (var tarWriter = new TarWriter(fileStream, TarEntryFormat.Pax, leaveOpen: true)) + { + var entry = new PaxTarEntry(TarEntryType.RegularFile, target) + { + DataStream = new MemoryStream("escaped"u8.ToArray()) + }; + tarWriter.WriteEntry(entry); + } + + Assert.False(ManagedArchive.ExtractTar(archivePath, Path.Combine(_testDir, "rooted-extract"))); + Assert.False(File.Exists(target)); + } + [Fact] public void ExtractTarGz_InvalidFile_ReturnsFalse() { diff --git a/Projects/UOContent/Compression/ManagedArchive.cs b/Projects/UOContent/Compression/ManagedArchive.cs index 32142d9e4..4b7e9da2b 100644 --- a/Projects/UOContent/Compression/ManagedArchive.cs +++ b/Projects/UOContent/Compression/ManagedArchive.cs @@ -224,9 +224,17 @@ public static class ManagedArchive { using var tarReader = new TarReader(stream); + var root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(outputDirectory)) + Path.DirectorySeparatorChar; + var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + while (tarReader.GetNextEntry() is { } entry) { - var destPath = Path.Combine(outputDirectory, entry.Name); + // Rooted names and ".." segments would otherwise let the archive write anywhere the process can. + var destPath = Path.GetFullPath(Path.Combine(root, entry.Name)); + if (!destPath.StartsWith(root, comparison)) + { + throw new InvalidDataException($"Archive entry '{entry.Name}' resolves outside '{root}'"); + } var destDir = Path.GetDirectoryName(destPath); if (destDir != null)