CVE-2021-32840: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: SharpZipLib (or #ziplib) is a Zip, GZip, Tar and BZip2 library. Prior to version 1.3.3, a TAR file entry `../evil.txt` may be extracted in the parent directory of `destFolder`. This leads to arbitrary file write that may lead to code execution. The vulnerability was patched in version 1.3.3.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 9.8
Published: 2022-01-26T21:15Z
Repository: https://github.com/icsharpcode/SharpZipLib
Language: C#
Files changed: test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj, test/ICSharpCode.SharpZipLib.Tests/Tar/TarArchiveTests.cs
Commit date: 2021-09-19 08:58:07 +0200
Commit message: test: add tests for tar path traversal

Diff: diff --git a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj
index fd6f61a..12183fc 100644
--- a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj
+++ b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj
@@ -3,10 +3,11 @@
   <PropertyGroup>
     <OutputType>Library</OutputType>
 		<TargetFrameworks>netcoreapp3.1;net46</TargetFrameworks>
     <ApplicationIcon />
     <StartupObject />
+		<LangVersion>8</LangVersion>
   </PropertyGroup>
 
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="nunit" Version="3.13.1" />
diff --git a/test/ICSharpCode.SharpZipLib.Tests/Tar/TarArchiveTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Tar/TarArchiveTests.cs
new file mode 100644
index 0000000..e7a2bcd
--- /dev/null
+++ b/test/ICSharpCode.SharpZipLib.Tests/Tar/TarArchiveTests.cs
@@ -0,0 +1,73 @@
+﻿using System.IO;
+using System.Text;
+using ICSharpCode.SharpZipLib.Core;
+using ICSharpCode.SharpZipLib.Tar;
+using static ICSharpCode.SharpZipLib.Tests.TestSupport.Utils;
+using NUnit.Framework;
+
+namespace ICSharpCode.SharpZipLib.Tests.Tar
+{
+	[TestFixture]
+	public class TarArchiveTests
+	{
+		[Test]
+		[Category("Tar")]
+		[Category("CreatesTempFile")]
+		public void ExtractingContentsWithNonTraversalPathSucceeds()
+		{
+			Assert.DoesNotThrow(() => ExtractTarOK("output", "test-good", allowTraverse: false));
+		}
+		
+		[Test]
+		[Category("Tar")]
+		[Category("CreatesTempFile")]
+		public void ExtractingContentsWithExplicitlyAllowedTraversalPathSucceeds()
+		{
+			Assert.DoesNotThrow(() => ExtractTarOK("output", "../file", allowTraverse: true));
+		}
+		
+		[Test]
+		[Category("Tar")]
+		[Category("CreatesTempFile")]
+		[TestCase("output", "../file")]
+		[TestCase("output", "../output.txt")]
+		public void ExtractingContentsWithDisallowedPathsFails(string outputDir, string fileName)
+		{
+			Assert.Throws<InvalidNameException>(() => ExtractTarOK(outputDir, fileName, allowTraverse: false));
+		}
+		
+		public void ExtractTarOK(string outputDir, string fileName, bool allowTraverse)
+		{
+			var fileContent = Encoding.UTF8.GetBytes("file content");
+			using var tempDir = new TempDir();
+			
+			var tempPath = tempDir.Fullpath;
+			var extractPath = Path.Combine(tempPath, outputDir);
+			var expectedOutputFile = Path.Combine(extractPath, fileName);
+
+			using var archiveStream = new MemoryStream();
+			
+			Directory.CreateDirectory(extractPath);
+
+			using (var tos = new TarOutputStream(archiveStream, Encoding.UTF8){IsStreamOwner = false})
+			{
+				var entry = TarEntry.CreateTarEntry(fileName);
+				entry.Size = fileContent.Length;
+				tos.PutNextEntry(entry);
+				tos.Write(fileContent, 0, fileContent.Length);
+				tos.CloseEntry();
+			}
+
+			archiveStream.Position = 0;
+
+			using (var ta = TarArchive.CreateInputTarArchive(archiveStream, Encoding.UTF8))
+			{
+				ta.ProgressMessageEvent += (archive, entry, message) 
+					=> TestContext.WriteLine($"{entry.Name} {entry.Size} {message}");
+				ta.ExtractContents(extractPath, allowTraverse);
+			}
+
+			Assert.That(File.Exists(expectedOutputFile));
+		}
+	}
+}

Fixed code: 		<LangVersion>8</LangVersion>
﻿using System.IO;
using System.Text;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Tar;
using static ICSharpCode.SharpZipLib.Tests.TestSupport.Utils;
using NUnit.Framework;

namespace ICSharpCode.SharpZipLib.Tests.Tar
{
	[TestFixture]
	public class TarArchiveTests
	{
		[Test]
		[Category("Tar")]
		[Category("CreatesTempFile")]
		public void ExtractingContentsWithNonTraversalPathSucceeds()
		{
			Assert.DoesNotThrow(() => ExtractTarOK("output", "test-good", allowTraverse: false));
		}
		
		[Test]
		[Category("Tar")]
		[Category("CreatesTempFile")]
		public void ExtractingContentsWithExplicitlyAllowedTraversalPathSucceeds()
		{
			Assert.DoesNotThrow(() => ExtractTarOK("output", "../file", allowTraverse: true));
		}
		
		[Test]
		[Category("Tar")]
		[Category("CreatesTempFile")]
		[TestCase("output", "../file")]
		[TestCase("output", "../output.txt")]
		public void ExtractingContentsWithDisallowedPathsFails(string outputDir, string fileName)
		{
			Assert.Throws<InvalidNameException>(() => ExtractTarOK(outputDir, fileName, allowTraverse: false));
		}
		
		public void ExtractTarOK(string outputDir, string fileName, bool allowTraverse)
		{
			var fileContent = Encoding.UTF8.GetBytes("file content");
			using var tempDir = new TempDir();
			
			var tempPath = tempDir.Fullpath;
			var extractPath = Path.Combine(tempPath, outputDir);
			var expectedOutputFile = Path.Combine(extractPath, fileName);

			using var archiveStream = new MemoryStream();
			
			Directory.CreateDirectory(extractPath);

			using (var tos = new TarOutputStream(archiveStream, Encoding.UTF8){IsStreamOwner = false})
			{
				var entry = TarEntry.CreateTarEntry(fileName);
				entry.Size = fileContent.Length;
				tos.PutNextEntry(entry);
				tos.Write(fileContent, 0, fileContent.Length);
				tos.CloseEntry();
			}

			archiveStream.Position = 0;

			using (var ta = TarArchive.CreateInputTarArchive(archiveStream, Encoding.UTF8))
			{
				ta.ProgressMessageEvent += (archive, entry, message) 
					=> TestContext.WriteLine($"{entry.Name} {entry.Size} {message}");
				ta.ExtractContents(extractPath, allowTraverse);
			}

			Assert.That(File.Exists(expectedOutputFile));
		}
	}
}