Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public int Extract(string packageFile, string directory)
if (IsExcludedPath(unescapedKey))
continue;

PackageExtractorUtils.ThrowIfPathTraversalAttempted(unescapedKey, directory);

var targetDirectory = Path.Combine(directory, Path.GetDirectoryName(unescapedKey) ?? string.Empty);

if (!Directory.Exists(targetDirectory))
Expand Down
16 changes: 16 additions & 0 deletions source/Calamari.Common/Features/Packages/PackageExtractorUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ public static class PackageExtractorUtils
{
public static void EnsureTargetDirectoryExists(string directory) => Directory.CreateDirectory(directory);

/// <summary>
/// Throws if the archive entry key would resolve to a path outside the intended extraction root (zip-slip / path traversal).
/// </summary>
public static void ThrowIfPathTraversalAttempted(string entryKey, string extractionDirectory)
{
var extractionRoot = Path.GetFullPath(extractionDirectory);
// Ensure the root ends with a separator so "root" can't be a prefix of "rootEvil"
if (!extractionRoot.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
extractionRoot += Path.DirectorySeparatorChar;

var destination = Path.GetFullPath(Path.Combine(extractionDirectory, entryKey));

if (!destination.StartsWith(extractionRoot, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"Archive entry '{entryKey}' would extract to '{destination}', which is outside the intended extraction directory '{extractionDirectory}'. The archive may be malicious.");
}

public static ResiliencePipeline CreateIoExceptionRetryStrategy(ILog log)
{
return new ResiliencePipelineBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public int Extract(string packageFile, string directory)
using var reader = TarReader.Open(compressionStream, new ReaderOptions { ArchiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 } });
while (reader.MoveToNextEntry())
{
if (reader.Entry.Key != null)
PackageExtractorUtils.ThrowIfPathTraversalAttempted(reader.Entry.Key, directory);
ProcessEvent(ref files, reader.Entry);
ExtractEntry(directory, reader);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public int Extract(string packageFile, string directory)

foreach (var entry in archive.Entries)
{
if (entry.Key != null)
PackageExtractorUtils.ThrowIfPathTraversalAttempted(entry.Key, directory);
ProcessEvent(ref filesExtracted, entry);
ExtractEntry(directory, entry);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,33 @@ public void ExtractIgnoresSymbolicLinks()
log.StandardOut.Should().ContainMatch("Cannot create symbolic link*");
}

[Test]
[TestCase(typeof(NupkgExtractor), "nupkg", ArchiveType.Zip, CompressionType.Deflate)]
[TestCase(typeof(ZipPackageExtractor), "zip", ArchiveType.Zip, CompressionType.Deflate)]
[TestCase(typeof(TarPackageExtractor), "tar", ArchiveType.Tar, CompressionType.None)]
[TestCase(typeof(TarGzipPackageExtractor), "tar.gz", ArchiveType.Tar, CompressionType.GZip)]
[TestCase(typeof(TarBzipPackageExtractor), "tar.bz2", ArchiveType.Tar, CompressionType.BZip2)]
public void ExtractThrowsWhenArchiveEntryAttemptsPathTraversal(Type extractorType, string extension, ArchiveType archiveType, CompressionType compressionType)
{
using var tempFolder = TemporaryDirectory.Create();
var packageFile = Path.Combine(tempFolder.DirectoryPath, $"malicious.{extension}");
var extractionDir = Path.Combine(tempFolder.DirectoryPath, "extraction");
Directory.CreateDirectory(extractionDir);

using (var stream = File.OpenWrite(packageFile))
using (var writer = WriterFactory.Open(stream, archiveType, new WriterOptions(compressionType) { ArchiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 } }))
{
var payload = "malicious content"u8.ToArray();
writer.Write("safe-file.txt", new MemoryStream(payload));
writer.Write("../traversal.txt", new MemoryStream(payload));
}

var extractor = (IPackageExtractor)Activator.CreateInstance(extractorType, ConsoleLog.Instance);

Assert.Throws<InvalidOperationException>(() => extractor.Extract(packageFile, extractionDir));
Assert.That(File.Exists(Path.Combine(tempFolder.DirectoryPath, "traversal.txt")), Is.False, "Traversal file should not have been written outside the extraction directory");
}

private string GetFileName(string extension)
{
return GetFixtureResource("Samples", string.Format("{0}.{1}.{2}", PackageId, PackageVersion, extension));
Expand Down