Skip to content
This repository was archived by the owner on Oct 4, 2024. It is now read-only.

Commit b8582b0

Browse files
committed
Prevent input file from creating files outside work directory
1 parent 3c38547 commit b8582b0

File tree

7 files changed

+87
-4
lines changed

7 files changed

+87
-4
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
<dependency>
7474
<groupId>org.apache.commons</groupId>
7575
<artifactId>commons-compress</artifactId>
76-
<version>1.10</version>
76+
<version>1.16.1</version>
7777
</dependency>
7878
<dependency>
7979
<groupId>joda-time</groupId>

src/main/java/com/amazonaws/codepipeline/jenkinsplugin/ExtractionTools.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private static void extractZipFile(final File destination, final ZipFile zipFile
6767

6868
while (entries.hasMoreElements()) {
6969
final ZipArchiveEntry entry = entries.nextElement();
70-
final File entryDestination = new File(destination, entry.getName());
70+
final File entryDestination = getDestinationFile(destination, entry.getName());
7171

7272
if (entry.isDirectory()) {
7373
entryDestination.mkdirs();
@@ -88,7 +88,7 @@ private static void extractArchive(final File destination, final ArchiveInputStr
8888
ArchiveEntry entry = archiveInputStream.getNextEntry();
8989

9090
while (entry != null) {
91-
final File destinationFile = new File(destination, entry.getName());
91+
final File destinationFile = getDestinationFile(destination, entry.getName());
9292

9393
if (entry.isDirectory()) {
9494
destinationFile.mkdir();
@@ -108,6 +108,14 @@ private static void extractArchive(final File destination, final ArchiveInputStr
108108
}
109109
}
110110

111+
private static File getDestinationFile(final File basedir, final String file) throws IOException {
112+
final File destination = new File(basedir, file);
113+
if (!destination.getCanonicalPath().startsWith(basedir.getCanonicalPath() + File.separator)) {
114+
throw new IOException("The compressed input file contains files targeting an invalid destination: " + file);
115+
}
116+
return destination;
117+
}
118+
111119
public static void deleteTemporaryCompressedFile(final File fileToDelete) throws IOException {
112120
if (fileToDelete.isDirectory()) {
113121
FileUtils.deleteDirectory(fileToDelete);

src/test/java/com/amazonaws/codepipeline/jenkinsplugin/ExtractionToolsTest.java

+76-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.amazonaws.codepipeline.jenkinsplugin;
1616

1717
import static org.junit.Assert.assertEquals;
18+
import static org.junit.Assert.assertTrue;
1819
import static org.junit.Assert.fail;
1920
import static org.mockito.Mockito.when;
2021

@@ -25,6 +26,7 @@
2526
import java.io.FileOutputStream;
2627
import java.io.IOException;
2728
import java.io.PrintWriter;
29+
import java.nio.file.FileSystems;
2830
import java.nio.file.FileVisitResult;
2931
import java.nio.file.Files;
3032
import java.nio.file.Path;
@@ -58,7 +60,8 @@
5860
@RunWith(ExtractionToolsTest.class)
5961
@Suite.SuiteClasses({
6062
ExtractionToolsTest.GetCompressionTypeTest.class,
61-
ExtractionToolsTest.DecompressFileTest.class
63+
ExtractionToolsTest.DecompressFileTest.class,
64+
ExtractionToolsTest.ExtractionPathTraversalTest.class
6265
})
6366
public class ExtractionToolsTest extends Suite {
6467

@@ -341,4 +344,76 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr
341344
}
342345
}
343346

347+
public static class ExtractionPathTraversalTest extends TestBase {
348+
private static final String BASE_DIR = "base";
349+
private Path testDir;
350+
351+
@Before
352+
public void setUp() throws IOException {
353+
testDir = Files.createTempDirectory("ExtractionPathTraversalTest.tmp.");
354+
}
355+
356+
@After
357+
public void tearDown() throws IOException {
358+
FileUtils.deleteDirectory(testDir.toFile());
359+
}
360+
361+
private void testPathTraversal(final String filename) throws IOException {
362+
final String filePath = getClass().getClassLoader().getResource(filename).getFile();
363+
final String osAppropriatePath = System.getProperty("os.name").contains("indow") ? filePath.substring(1) : filePath;
364+
final Path cliCompressedFile = Paths.get(osAppropriatePath);
365+
final Path baseDir = FileSystems.getDefault().getPath(testDir.toFile().getAbsolutePath(), BASE_DIR);
366+
Files.createDirectories(baseDir);
367+
368+
// path traversal is dependent on zip file contents and the file system in use, for that reason the test may
369+
// or may not throw an IOException. But, in either case, no file should be produced on top level testDir.
370+
try {
371+
ExtractionTools.decompressFile(
372+
cliCompressedFile.toFile(),
373+
baseDir.toFile(),
374+
CompressionType.Zip,
375+
null);
376+
} catch (final IOException e) {
377+
assertTrue(e.getMessage().startsWith("The compressed input file contains files targeting an invalid destination: "));
378+
}
379+
final String[] files = testDir.toFile().list();
380+
assertEquals(filename + " should produce no extra entries in test dir after extraction", 1, files.length);
381+
assertEquals("the only entry in test dir should be '" + BASE_DIR + "' directory", BASE_DIR, files[0]);
382+
assertTrue(filename + " should produce a file named good.txt in '" + BASE_DIR + "' directory",
383+
Files.exists(baseDir.resolve("good.txt")));
384+
}
385+
386+
@Test
387+
public void shouldNotTraverseBaseDirOnExtractionUnix() throws IOException {
388+
// dir-traversal-unix.zip:
389+
// - good.txt
390+
// - ../evil.txt
391+
testPathTraversal("dir-traversal-unix.zip");
392+
}
393+
394+
@Test
395+
public void shouldNotTraverseBaseDirOnExtractionUnixIfFilenameMatchesBaseDir() throws IOException {
396+
// dir-traversal-unix2.zip:
397+
// - good.txt
398+
// - ../base-evil.txt
399+
testPathTraversal("dir-traversal-unix2.zip");
400+
}
401+
402+
@Test
403+
public void shouldNotTraverseBaseDirOnExtractionWin() throws IOException {
404+
// dir-traversal-win.zip:
405+
// - good.txt
406+
// - ..\evil.txt
407+
testPathTraversal("dir-traversal-win.zip");
408+
}
409+
410+
@Test
411+
public void shouldNotTraverseBaseDirOnExtractionWinIfFilenameMatchesBaseDir() throws IOException {
412+
// dir-traversal-win2.zip:
413+
// - good.txt
414+
// - ..\base-evil.txt
415+
testPathTraversal("dir-traversal-win2.zip");
416+
}
417+
}
418+
344419
}
316 Bytes
Binary file not shown.
328 Bytes
Binary file not shown.
316 Bytes
Binary file not shown.
328 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)