From 9dc7f16b06b9047280bae27940aed5fdf030dcdf Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Mon, 14 May 2018 14:57:46 +0200 Subject: [PATCH] SONAR-10661 fix vulnerability in ZipUtils#unzip() --- .../java/org/sonar/api/utils/ZipUtils.java | 24 ++++++++--- .../org/sonar/api/utils/ZipUtilsTest.java | 43 +++++++++++++++---- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/ZipUtils.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/ZipUtils.java index 812d317e88d..5477600ad5a 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/utils/ZipUtils.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/ZipUtils.java @@ -26,6 +26,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Path; import java.util.Enumeration; import java.util.function.Predicate; import java.util.zip.ZipEntry; @@ -101,6 +102,8 @@ public final class ZipUtils { private static void unzipEntry(ZipEntry entry, ZipInputStream zipStream, File toDir) throws IOException { File to = new File(toDir, entry.getName()); + verifyInsideTargetDirectory(entry, to.toPath(), toDir.toPath()); + if (entry.isDirectory()) { throwExceptionIfDirectoryIsNotCreatable(to); } else { @@ -139,19 +142,23 @@ public final class ZipUtils { FileUtils.forceMkdir(toDir); } + Path targetDirNormalizedPath = toDir.toPath().normalize(); ZipFile zipFile = new ZipFile(zip); try { Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (filter.test(entry)) { - File to = new File(toDir, entry.getName()); + File target = new File(toDir, entry.getName()); + + verifyInsideTargetDirectory(entry, target.toPath(), targetDirNormalizedPath); + if (entry.isDirectory()) { - throwExceptionIfDirectoryIsNotCreatable(to); + throwExceptionIfDirectoryIsNotCreatable(target); } else { - File parent = to.getParentFile(); + File parent = target.getParentFile(); throwExceptionIfDirectoryIsNotCreatable(parent); - copy(zipFile, entry, to); + copy(zipFile, entry, target); } } } @@ -238,6 +245,13 @@ public final class ZipUtils { } } + private static void verifyInsideTargetDirectory(ZipEntry entry, Path entryPath, Path targetDirPath) { + if (!entryPath.normalize().startsWith(targetDirPath.normalize())) { + // vulnerability - trying to create a file outside the target directory + throw new IllegalStateException("Unzipping an entry outside the target directory is not allowed: " + entry.getName()); + } + } + /** * @see #unzip(File, File, Predicate) * @deprecated replaced by {@link Predicate} in 6.2. @@ -260,4 +274,4 @@ public final class ZipUtils { return delegate.accept(zipEntry); } } -} +} \ No newline at end of file diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/utils/ZipUtilsTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/utils/ZipUtilsTest.java index a6f0414c57a..e475a50336d 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/utils/ZipUtilsTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/utils/ZipUtilsTest.java @@ -20,19 +20,20 @@ package org.sonar.api.utils; import com.google.common.collect.Iterators; -import java.net.URL; -import org.apache.commons.io.FileUtils; -import org.assertj.core.util.Files; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.util.Iterator; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.apache.commons.io.FileUtils; +import org.assertj.core.util.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +41,8 @@ public class ZipUtilsTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); @Test public void zip_directory() throws IOException { @@ -106,7 +109,31 @@ public class ZipUtilsTest { assertThat(toDir.listFiles()).containsOnly(new File(toDir, "foo.txt")); } + @Test + public void fail_if_unzipping_file_outside_target_directory() throws Exception { + File zip = new File(getClass().getResource("ZipUtilsTest/zip-slip.zip").toURI()); + File toDir = temp.newFolder(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Unzipping an entry outside the target directory is not allowed: ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt"); + + ZipUtils.unzip(zip, toDir); + } + + @Test + public void fail_if_unzipping_stream_outside_target_directory() throws Exception { + File zip = new File(getClass().getResource("ZipUtilsTest/zip-slip.zip").toURI()); + File toDir = temp.newFolder(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Unzipping an entry outside the target directory is not allowed: ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt"); + + try (InputStream input = new FileInputStream(zip)) { + ZipUtils.unzip(input, toDir); + } + } + private URL urlToZip() { return getClass().getResource("/org/sonar/api/utils/ZipUtilsTest/shouldUnzipFile.zip"); } -} +} \ No newline at end of file -- 2.39.5