Skip to content

Commit 5376c2d

Browse files
authored
Restrict path traversal on FastZip extraction (#235)
Fixes #232 - Prevent traversal outside of extraction directory - Add new explicit exception for invalid names - Add tests for extraction path traversal Note: Use new parameter `allowParentTraversal` to re-enable past behaviour
1 parent e3aa36d commit 5376c2d

File tree

4 files changed

+139
-12
lines changed

4 files changed

+139
-12
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace ICSharpCode.SharpZipLib.Core
6+
{
7+
8+
/// <summary>
9+
/// InvalidNameException is thrown for invalid names such as directory traversal paths and names with invalid characters
10+
/// </summary>
11+
public class InvalidNameException: SharpZipBaseException
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the InvalidNameException class with a default error message.
15+
/// </summary>
16+
public InvalidNameException(): base("An invalid name was specified")
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Initializes a new instance of the InvalidNameException class with a specified error message.
22+
/// </summary>
23+
/// <param name="message">A message describing the exception.</param>
24+
public InvalidNameException(string message) : base(message)
25+
{
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the InvalidNameException class with a specified
30+
/// error message and a reference to the inner exception that is the cause of this exception.
31+
/// </summary>
32+
/// <param name="message">A message describing the exception.</param>
33+
/// <param name="innerException">The inner exception</param>
34+
public InvalidNameException(string message, Exception innerException) : base(message, innerException)
35+
{
36+
}
37+
}
38+
}

src/ICSharpCode.SharpZipLib/Zip/FastZip.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,12 +385,13 @@ public void ExtractZip(string zipFileName, string targetDirectory, string fileFi
385385
/// <param name="fileFilter">A filter to apply to files.</param>
386386
/// <param name="directoryFilter">A filter to apply to directories.</param>
387387
/// <param name="restoreDateTime">Flag indicating whether to restore the date and time for extracted files.</param>
388+
/// <param name="allowParentTraversal">Allow parent directory traversal in file paths (e.g. ../file)</param>
388389
public void ExtractZip(string zipFileName, string targetDirectory,
389390
Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate,
390-
string fileFilter, string directoryFilter, bool restoreDateTime)
391+
string fileFilter, string directoryFilter, bool restoreDateTime, bool allowParentTraversal = false)
391392
{
392393
Stream inputStream = File.Open(zipFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
393-
ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true);
394+
ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true, allowParentTraversal);
394395
}
395396

396397
/// <summary>
@@ -404,10 +405,11 @@ public void ExtractZip(string zipFileName, string targetDirectory,
404405
/// <param name="directoryFilter">A filter to apply to directories.</param>
405406
/// <param name="restoreDateTime">Flag indicating whether to restore the date and time for extracted files.</param>
406407
/// <param name="isStreamOwner">Flag indicating whether the inputStream will be closed by this method.</param>
408+
/// <param name="allowParentTraversal">Allow parent directory traversal in file paths (e.g. ../file)</param>
407409
public void ExtractZip(Stream inputStream, string targetDirectory,
408410
Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate,
409411
string fileFilter, string directoryFilter, bool restoreDateTime,
410-
bool isStreamOwner)
412+
bool isStreamOwner, bool allowParentTraversal = false)
411413
{
412414
if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null)) {
413415
throw new ArgumentNullException(nameof(confirmDelegate));
@@ -416,7 +418,7 @@ public void ExtractZip(Stream inputStream, string targetDirectory,
416418
continueRunning_ = true;
417419
overwrite_ = overwrite;
418420
confirmDelegate_ = confirmDelegate;
419-
extractNameTransform_ = new WindowsNameTransform(targetDirectory);
421+
extractNameTransform_ = new WindowsNameTransform(targetDirectory, allowParentTraversal);
420422

421423
fileFilter_ = new NameFilter(fileFilter);
422424
directoryFilter_ = new NameFilter(directoryFilter);

src/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class WindowsNameTransform : INameTransform
1919
string _baseDirectory;
2020
bool _trimIncomingPaths;
2121
char _replacementChar = '_';
22+
private bool _allowParentTraversal;
2223

2324
/// <summary>
2425
/// In this case we need Windows' invalid path characters.
@@ -38,13 +39,11 @@ public class WindowsNameTransform : INameTransform
3839
/// Initialises a new instance of <see cref="WindowsNameTransform"/>
3940
/// </summary>
4041
/// <param name="baseDirectory"></param>
41-
public WindowsNameTransform(string baseDirectory)
42+
/// <param name="allowParentTraversal">Allow parent directory traversal in file paths (e.g. ../file)</param>
43+
public WindowsNameTransform(string baseDirectory, bool allowParentTraversal = false)
4244
{
43-
if (baseDirectory == null) {
44-
throw new ArgumentNullException(nameof(baseDirectory), "Directory name is invalid");
45-
}
46-
47-
BaseDirectory = baseDirectory;
45+
BaseDirectory = baseDirectory ?? throw new ArgumentNullException(nameof(baseDirectory), "Directory name is invalid");
46+
AllowParentTraversal = allowParentTraversal;
4847
}
4948

5049
/// <summary>
@@ -69,6 +68,15 @@ public string BaseDirectory {
6968
}
7069
}
7170

71+
/// <summary>
72+
/// Allow parent directory traversal in file paths (e.g. ../file)
73+
/// </summary>
74+
public bool AllowParentTraversal
75+
{
76+
get => _allowParentTraversal;
77+
set => _allowParentTraversal = value;
78+
}
79+
7280
/// <summary>
7381
/// Gets or sets a value indicating wether paths on incoming values should be removed.
7482
/// </summary>
@@ -90,7 +98,7 @@ public string TransformDirectory(string name)
9098
name = name.Remove(name.Length - 1, 1);
9199
}
92100
} else {
93-
throw new ZipException("Cannot have an empty directory name");
101+
throw new InvalidNameException("Cannot have an empty directory name");
94102
}
95103
return name;
96104
}
@@ -113,6 +121,11 @@ public string TransformFile(string name)
113121
// Combine will throw a PathTooLongException in that case.
114122
if (_baseDirectory != null) {
115123
name = Path.Combine(_baseDirectory, name);
124+
125+
if(!_allowParentTraversal && !Path.GetFullPath(name).StartsWith(_baseDirectory, StringComparison.InvariantCultureIgnoreCase))
126+
{
127+
throw new InvalidNameException("Parent traversal in paths is not allowed");
128+
}
116129
}
117130
} else {
118131
name = string.Empty;

test/ICSharpCode.SharpZipLib.Tests/Zip/FastZipHandling.cs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.IO;
1+
using System;
2+
using System.IO;
23
using System.Text.RegularExpressions;
34
using ICSharpCode.SharpZipLib.Zip;
45
using NUnit.Framework;
@@ -269,5 +270,78 @@ public void NonAsciiPasswords()
269270
File.Delete(tempName1);
270271
}
271272
}
273+
274+
275+
[Test]
276+
[Category("Zip")]
277+
[Category("CreatesTempFile")]
278+
public void LimitExtractPath()
279+
{
280+
string tempPath = GetTempFilePath();
281+
Assert.IsNotNull(tempPath, "No permission to execute this test?");
282+
283+
var uniqueName = "SharpZipLib.Test_" + DateTime.Now.Ticks.ToString("x");
284+
285+
tempPath = Path.Combine(tempPath, uniqueName);
286+
var extractPath = Path.Combine(tempPath, "output");
287+
288+
const string contentFile = "content.txt";
289+
290+
var contentFilePathBad = Path.Combine("..", contentFile);
291+
var extractFilePathBad = Path.Combine(tempPath, contentFile);
292+
var archiveFileBad = Path.Combine(tempPath, "test-good.zip");
293+
294+
var contentFilePathGood = Path.Combine("childDir", contentFile);
295+
var extractFilePathGood = Path.Combine(extractPath, contentFilePathGood);
296+
var archiveFileGood = Path.Combine(tempPath, "test-bad.zip");
297+
298+
try
299+
{
300+
Directory.CreateDirectory(extractPath);
301+
302+
// Create test input
303+
void CreateTestFile(string archiveFile, string contentPath)
304+
{
305+
using (var zf = ZipFile.Create(archiveFile))
306+
{
307+
zf.BeginUpdate();
308+
zf.Add(new StringMemoryDataSource($"Content of {archiveFile}"), contentPath);
309+
zf.CommitUpdate();
310+
}
311+
}
312+
313+
CreateTestFile(archiveFileGood, contentFilePathGood);
314+
CreateTestFile(archiveFileBad, contentFilePathBad);
315+
316+
Assert.IsTrue(File.Exists(archiveFileGood), "Good test archive was not created");
317+
Assert.IsTrue(File.Exists(archiveFileBad), "Bad test archive was not created");
318+
319+
var fastZip = new FastZip();
320+
321+
Assert.DoesNotThrow(() => {
322+
fastZip.ExtractZip(archiveFileGood, extractPath, "");
323+
}, "Threw exception on good file name");
324+
325+
Assert.IsTrue(File.Exists(extractFilePathGood), "Good output file not created");
326+
327+
Assert.Throws<SharpZipLib.Core.InvalidNameException>(() => {
328+
fastZip.ExtractZip(archiveFileBad, extractPath, "");
329+
}, "No exception was thrown for bad file name");
330+
331+
Assert.IsFalse(File.Exists(extractFilePathBad), "Bad output file created");
332+
333+
Assert.DoesNotThrow(() => {
334+
fastZip.ExtractZip(archiveFileBad, extractPath, FastZip.Overwrite.Never, null, "", "", true, true);
335+
}, "Threw exception on bad file name when traversal explicitly allowed");
336+
337+
Assert.IsTrue(File.Exists(extractFilePathBad), "Bad output file not created when traversal explicitly allowed");
338+
339+
}
340+
finally
341+
{
342+
Directory.Delete(tempPath, true);
343+
}
344+
}
345+
272346
}
273347
}

0 commit comments

Comments
 (0)