Skip to content

Commit 08438a2

Browse files
Simon BrandhofSonarTech
authored andcommitted
SONAR-10661 fix vulnerability in ZipUtils#unzip()
1 parent 78cddc3 commit 08438a2

3 files changed

Lines changed: 52 additions & 11 deletions

File tree

sonar-plugin-api/src/main/java/org/sonar/api/utils/ZipUtils.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.io.IOException;
2727
import java.io.InputStream;
2828
import java.io.OutputStream;
29+
import java.nio.file.Path;
2930
import java.util.Enumeration;
3031
import java.util.function.Predicate;
3132
import java.util.zip.ZipEntry;
@@ -101,6 +102,8 @@ public static File unzip(InputStream stream, File toDir, Predicate<ZipEntry> fil
101102

102103
private static void unzipEntry(ZipEntry entry, ZipInputStream zipStream, File toDir) throws IOException {
103104
File to = new File(toDir, entry.getName());
105+
verifyInsideTargetDirectory(entry, to.toPath(), toDir.toPath());
106+
104107
if (entry.isDirectory()) {
105108
throwExceptionIfDirectoryIsNotCreatable(to);
106109
} else {
@@ -139,19 +142,23 @@ public static File unzip(File zip, File toDir, Predicate<ZipEntry> filter) throw
139142
FileUtils.forceMkdir(toDir);
140143
}
141144

145+
Path targetDirNormalizedPath = toDir.toPath().normalize();
142146
ZipFile zipFile = new ZipFile(zip);
143147
try {
144148
Enumeration<? extends ZipEntry> entries = zipFile.entries();
145149
while (entries.hasMoreElements()) {
146150
ZipEntry entry = entries.nextElement();
147151
if (filter.test(entry)) {
148-
File to = new File(toDir, entry.getName());
152+
File target = new File(toDir, entry.getName());
153+
154+
verifyInsideTargetDirectory(entry, target.toPath(), targetDirNormalizedPath);
155+
149156
if (entry.isDirectory()) {
150-
throwExceptionIfDirectoryIsNotCreatable(to);
157+
throwExceptionIfDirectoryIsNotCreatable(target);
151158
} else {
152-
File parent = to.getParentFile();
159+
File parent = target.getParentFile();
153160
throwExceptionIfDirectoryIsNotCreatable(parent);
154-
copy(zipFile, entry, to);
161+
copy(zipFile, entry, target);
155162
}
156163
}
157164
}
@@ -238,6 +245,13 @@ private static void doZipDir(File dir, ZipOutputStream out) throws IOException {
238245
}
239246
}
240247

248+
private static void verifyInsideTargetDirectory(ZipEntry entry, Path entryPath, Path targetDirPath) {
249+
if (!entryPath.normalize().startsWith(targetDirPath.normalize())) {
250+
// vulnerability - trying to create a file outside the target directory
251+
throw new IllegalStateException("Unzipping an entry outside the target directory is not allowed: " + entry.getName());
252+
}
253+
}
254+
241255
/**
242256
* @see #unzip(File, File, Predicate)
243257
* @deprecated replaced by {@link Predicate<ZipEntry>} in 6.2.

sonar-plugin-api/src/test/java/org/sonar/api/utils/ZipUtilsTest.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,29 @@
2020
package org.sonar.api.utils;
2121

2222
import com.google.common.collect.Iterators;
23-
import java.net.URL;
24-
import org.apache.commons.io.FileUtils;
25-
import org.assertj.core.util.Files;
26-
import org.junit.Rule;
27-
import org.junit.Test;
28-
import org.junit.rules.TemporaryFolder;
29-
3023
import java.io.File;
24+
import java.io.FileInputStream;
3125
import java.io.IOException;
3226
import java.io.InputStream;
27+
import java.net.URL;
3328
import java.util.Iterator;
3429
import java.util.zip.ZipEntry;
3530
import java.util.zip.ZipFile;
31+
import org.apache.commons.io.FileUtils;
32+
import org.assertj.core.util.Files;
33+
import org.junit.Rule;
34+
import org.junit.Test;
35+
import org.junit.rules.ExpectedException;
36+
import org.junit.rules.TemporaryFolder;
3637

3738
import static org.assertj.core.api.Assertions.assertThat;
3839

3940
public class ZipUtilsTest {
4041

4142
@Rule
4243
public TemporaryFolder temp = new TemporaryFolder();
44+
@Rule
45+
public ExpectedException expectedException = ExpectedException.none();
4346

4447
@Test
4548
public void zip_directory() throws IOException {
@@ -106,6 +109,30 @@ public void unzipping_stream_extracts_subset_of_files() throws IOException {
106109
assertThat(toDir.listFiles()).containsOnly(new File(toDir, "foo.txt"));
107110
}
108111

112+
@Test
113+
public void fail_if_unzipping_file_outside_target_directory() throws Exception {
114+
File zip = new File(getClass().getResource("ZipUtilsTest/zip-slip.zip").toURI());
115+
File toDir = temp.newFolder();
116+
117+
expectedException.expect(IllegalStateException.class);
118+
expectedException.expectMessage("Unzipping an entry outside the target directory is not allowed: ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt");
119+
120+
ZipUtils.unzip(zip, toDir);
121+
}
122+
123+
@Test
124+
public void fail_if_unzipping_stream_outside_target_directory() throws Exception {
125+
File zip = new File(getClass().getResource("ZipUtilsTest/zip-slip.zip").toURI());
126+
File toDir = temp.newFolder();
127+
128+
expectedException.expect(IllegalStateException.class);
129+
expectedException.expectMessage("Unzipping an entry outside the target directory is not allowed: ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt");
130+
131+
try (InputStream input = new FileInputStream(zip)) {
132+
ZipUtils.unzip(input, toDir);
133+
}
134+
}
135+
109136
private URL urlToZip() {
110137
return getClass().getResource("/org/sonar/api/utils/ZipUtilsTest/shouldUnzipFile.zip");
111138
}
Binary file not shown.

0 commit comments

Comments
 (0)