Skip to content

Commit b4e07f8

Browse files
authored
Merge pull request cdapio#16024 from cdapio/CDAP-21193_zipslip
[CDAP-21193] : Check and validate each entry of uploaded jar that is within destination dir before writing
2 parents cec9b97 + e8c69ed commit b4e07f8

File tree

2 files changed

+95
-26
lines changed

2 files changed

+95
-26
lines changed

cdap-common/src/main/java/io/cdap/cdap/common/lang/jar/BundleJarUtil.java

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@
3434
import java.nio.file.SimpleFileVisitor;
3535
import java.nio.file.StandardCopyOption;
3636
import java.nio.file.attribute.BasicFileAttributes;
37+
import java.util.Comparator;
3738
import java.util.EnumSet;
3839
import java.util.function.Predicate;
3940
import java.util.jar.JarEntry;
4041
import java.util.jar.JarFile;
4142
import java.util.jar.JarInputStream;
4243
import java.util.jar.JarOutputStream;
4344
import java.util.jar.Manifest;
45+
import java.util.stream.Stream;
4446
import java.util.zip.ZipEntry;
4547
import java.util.zip.ZipInputStream;
4648
import java.util.zip.ZipOutputStream;
@@ -168,13 +170,13 @@ public static void addToArchive(final File input, final boolean includeDirName,
168170
public static void addToArchive(final File input, final boolean includeDirName,
169171
final ZipOutputStream output,
170172
final Predicate<String> fileNameFilter) throws IOException {
171-
final URI baseURI = input.toURI();
173+
final URI baseUri = input.toURI();
172174
Files.walkFileTree(input.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS),
173175
Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
174176
@Override
175177
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
176178
throws IOException {
177-
URI uri = baseURI.relativize(dir.toUri());
179+
URI uri = baseUri.relativize(dir.toUri());
178180
String entryName =
179181
includeDirName ? input.getName() + "/" + uri.getPath() : uri.getPath();
180182

@@ -191,7 +193,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
191193
if (fileNameFilter.test(file.getFileName().toString())) {
192194
return FileVisitResult.CONTINUE;
193195
}
194-
URI uri = baseURI.relativize(file.toUri());
196+
URI uri = baseUri.relativize(file.toUri());
195197
if (uri.getPath().isEmpty()) {
196198
// Only happen if the given "input" is a file.
197199
output.putNextEntry(new ZipEntry(file.toFile().getName()));
@@ -210,7 +212,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
210212
* Takes a jar or a local directory and prepares a folder to be loaded by classloader. If a jar is
211213
* provided it unpacks a manifest and any nested jars and links original jar into the destination
212214
* folder, so that it would be picked up by classloader to load any classes or resources.
213-
*
215+
* <p>
214216
* If a directory is provided, it assumes that this directory already contains the unpacked jar
215217
* contents (ie. this directory was used as the destinationFolder in a previous call to this
216218
* method). In this case, no unpacking is needed. The {@link ClassLoaderFolder} returned will be
@@ -314,6 +316,58 @@ private static File unJar(File jarFile, File destinationFolder, Predicate<String
314316
return destinationFolder;
315317
}
316318

319+
private static void unJar(ZipInputStream input, File targetDirectory,
320+
Predicate<String> nameFilter,
321+
CopyOption... copyOptions)
322+
throws IOException {
323+
324+
Path targetPath = targetDirectory.toPath().normalize();
325+
boolean isSuccess = false;
326+
boolean isTargetDirectoryNewlyCreated = false;
327+
328+
try {
329+
// Check if the directory exists before trying to create it
330+
if (!Files.exists(targetPath)) {
331+
Files.createDirectories(targetPath);
332+
isTargetDirectoryNewlyCreated = true;
333+
}
334+
335+
ZipEntry entry;
336+
while ((entry = input.getNextEntry()) != null) {
337+
String entryName = entry.getName();
338+
339+
if (!nameFilter.test(entryName)) {
340+
continue;
341+
}
342+
343+
Path output = targetPath.resolve(entryName).normalize();
344+
345+
if (!output.startsWith(targetPath)) {
346+
throw new IllegalArgumentException("Illegal path detected for Jar Entry : " + entryName + ". This will try to"
347+
+ " write outside the target directory, which is not allowed.");
348+
}
349+
350+
if (entry.isDirectory()) {
351+
Files.createDirectories(output);
352+
} else {
353+
Files.createDirectories(output.getParent());
354+
Files.copy(input, output, copyOptions);
355+
}
356+
input.closeEntry();
357+
}
358+
isSuccess = true;
359+
} finally {
360+
// Cleanup logic: Only delete if not successful AND we created the directory
361+
if (!isSuccess && isTargetDirectoryNewlyCreated && Files.exists(targetPath)) {
362+
try (Stream<Path> paths = Files.walk(targetPath)) {
363+
paths.sorted(Comparator.reverseOrder())
364+
.map(Path::toFile)
365+
.forEach(File::delete);
366+
}
367+
}
368+
}
369+
}
370+
317371
/**
318372
* Search for {@link Manifest} from the given {@link JarInputStream}.
319373
*
@@ -341,28 +395,6 @@ private static Manifest getManifest(JarInputStream jarInput) throws IOException
341395
return null;
342396
}
343397

344-
private static void unJar(ZipInputStream input, File targetDirectory,
345-
Predicate<String> nameFilter,
346-
CopyOption... copyOptions)
347-
throws IOException {
348-
Path targetPath = targetDirectory.toPath();
349-
Files.createDirectories(targetPath);
350-
351-
ZipEntry entry;
352-
while ((entry = input.getNextEntry()) != null) {
353-
if (nameFilter.test(entry.getName())) {
354-
Path output = targetPath.resolve(entry.getName());
355-
356-
if (entry.isDirectory()) {
357-
Files.createDirectories(output);
358-
} else {
359-
Files.createDirectories(output.getParent());
360-
Files.copy(input, output, copyOptions);
361-
}
362-
}
363-
}
364-
}
365-
366398
private BundleJarUtil() {
367399
}
368400
}

cdap-common/src/test/java/io/cdap/cdap/common/lang/jar/BundleJarUtilTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.common.io.CharStreams;
2222
import com.google.common.io.Files;
2323
import io.cdap.cdap.common.io.Locations;
24+
import java.io.ByteArrayOutputStream;
2425
import java.io.File;
2526
import java.io.FileOutputStream;
2627
import java.io.IOException;
@@ -32,16 +33,23 @@
3233
import java.util.jar.JarEntry;
3334
import java.util.jar.JarFile;
3435
import java.util.jar.JarOutputStream;
36+
import java.util.zip.ZipEntry;
37+
import java.util.zip.ZipOutputStream;
3538
import org.junit.Assert;
3639
import org.junit.ClassRule;
40+
import org.junit.Rule;
3741
import org.junit.Test;
42+
import org.junit.rules.ExpectedException;
3843
import org.junit.rules.TemporaryFolder;
3944

4045
/**
4146
* Unit Tests for {@link BundleJarUtil}.
4247
*/
4348
public class BundleJarUtilTest {
4449

50+
@Rule
51+
public ExpectedException expectedEx = ExpectedException.none();
52+
4553
@ClassRule
4654
public static final TemporaryFolder TEMP_FOLDER = new TemporaryFolder();
4755

@@ -159,6 +167,35 @@ public boolean test(String name) {
159167
}
160168
}
161169

170+
171+
@Test
172+
public void testZipSlipValidation() throws IOException {
173+
174+
expectedEx.expect(IllegalArgumentException.class);
175+
expectedEx.expectMessage("Illegal path detected for Jar Entry : ../../../malicious.jar. This will try to "
176+
+ "write outside the target directory, which is not allowed.");
177+
178+
// Create a file inside a sub-dir.
179+
File inputJarDirectory = TEMP_FOLDER.newFolder();
180+
181+
String maliciousEntryName = "../../../malicious.jar";
182+
183+
// 1. Create malicious JAR content in memory
184+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
185+
try (ZipOutputStream zos = new ZipOutputStream(bos)) {
186+
ZipEntry maliciousEntry = new ZipEntry(maliciousEntryName);
187+
zos.putNextEntry(maliciousEntry);
188+
zos.write("some random message!".getBytes());
189+
zos.closeEntry();
190+
}
191+
byte[] maliciousJarContent = bos.toByteArray();
192+
File maliciousJarFile = new File(inputJarDirectory, "malicious.jar");
193+
java.nio.file.Files.createDirectories(inputJarDirectory.toPath());
194+
java.nio.file.Files.write(maliciousJarFile.toPath(), maliciousJarContent);
195+
196+
BundleJarUtil.unJar(maliciousJarFile, TEMP_FOLDER.newFolder());
197+
}
198+
162199
/**
163200
* Helper method to recursively check if two directories are equal
164201
*/

0 commit comments

Comments
 (0)