Safeguard
Application Security

Detecting and preventing Zip Slip and path traversal in Java

Snyk's 2018 Zip Slip disclosure hit Amazon, Apache, and LinkedIn projects at once — here's how the flaw still hides in Java archive code today, and how to catch it.

Safeguard Research Team
Research
5 min read

On June 5, 2018, Snyk's security research team disclosed a single vulnerability pattern present in archive-handling code across projects from Amazon, Apache, Pivotal, LinkedIn, Twitter's Apache Heron, Alibaba's JStorm, Jenkins, and Gradle. They named it Zip Slip, and the root cause was almost embarrassingly simple: java.util.zip.ZipEntry.getName() returns whatever path string is stored inside the archive, with zero validation, and code that does new File(destDir, entry.getName()) will happily write to any location a crafted entry name points to — including ../../../etc/cron.d/evil climbing straight out of the intended extraction directory. One tracked instance, CVE-2018-1002201 in the zeroturnaround/zt-zip library, was fixed in version 1.13. Zip Slip is really just CWE-22 path traversal wearing an archive-extraction costume, and eight years later the same unguarded getName() call still shows up in new Java codebases, because neither the JDK's ZipInputStream/ZipFile classes nor popular alternatives like Apache Commons Compress, zt-zip, or zip4j reject traversal sequences by default. This post covers why the bug persists, how to actually verify a fix, and how static analysis catches it before it ships.

Why does Java keep reintroducing the same archive-extraction bug?

Java keeps reintroducing Zip Slip because, as Snyk's researchers noted in their original write-up, the language has no single canonical high-level archive library — teams reach for java.util.zip, Apache Commons Compress, zt-zip, or zip4j interchangeably, and every one of them exposes the raw, unvalidated entry name from the archive rather than a pre-sanitized path. Each extraction loop is typically hand-written: open the stream, iterate entries, resolve a destination File, write bytes. That resolution step is where the check has to live, and it's easy to skip under deadline pressure because the code compiles, runs, and passes a functional test with a well-behaved test ZIP that nobody thinks to poison with ../ sequences. Android's own developer security documentation independently calls out "Zip Path Traversal" as a recurring class in apps built on java.util.zip, which shows the pattern isn't confined to server-side Java — it reappears anywhere the JDK's zip APIs get used directly.

What does a real Zip Slip payload look like in practice?

A real Zip Slip payload is just an archive entry whose stored name contains directory-traversal segments instead of a normal relative filename — for example an entry named ../../../../etc/cron.d/malicious-cron or, on a server running as a privileged account, a path that lands inside a startup-script directory or a web root's executable path. When vulnerable code calls entry.getName() and passes it straight into new File(destDir, name), the resulting File object's path is destDir/../../../../etc/cron.d/malicious-cron, which resolves outside destDir entirely once the filesystem walks the .. segments. Because extraction routines usually run with whatever privileges the extracting process has — a CI job, a plugin loader, a file-upload handler — an attacker who can get their crafted archive extracted can potentially achieve remote code execution, not just arbitrary file write, depending on where the written file lands and what re-reads it afterward.

How do you actually verify a fix works, rather than just adding a check?

Verifying the fix means confirming the resolved path is provably inside the destination directory, not just that a string looks safe. The standard pattern, matching what OWASP and Snyk both publish: resolve the target file (new File(destDir, entry.getName()) or destDir.resolve(entry.getName()) with NIO), then canonicalize it with getCanonicalFile() or toPath().normalize(), and check that the canonical destination directory path is a true prefix of the canonicalized target — using targetPath.startsWith(destDirPath) on Path objects (which compares path segments), never a raw string startsWith() call, which a sibling directory like /dest-evil will falsely pass. Reject any entry that fails the check, that carries an absolute path, or that resolves through a symlink outside the destination. Add entry-count and total-decompressed-size limits in the same loop — that also blocks zip-bomb denial-of-service, a related but distinct risk in the same code path.

Does Java 21's newer APIs fix this automatically?

No. Even in current LTS Java (21, released September 2023, and the subsequent releases through 2026), java.util.zip.ZipInputStream and ZipFile still return the raw stored entry name with no built-in traversal filtering — there is no JDK-level equivalent of the filter parameter Python 3.12 added to tarfile.extractall() for the same class of bug. Apache Commons Compress, zt-zip (post-1.13), and zip4j have each shipped safer helper methods or documentation warning about the pattern since the 2018 disclosure, but none of them make the unsafe call path unreachable — a developer can still call the low-level API directly and skip the check. That means the canonicalization-and-prefix-check pattern isn't legacy advice for old Java versions; it is still the only reliable fix in 2026, and it has to be applied by hand in every extraction routine a codebase writes.

How does Safeguard help?

Safeguard's SAST engine covers Java as one of its phase-1 languages and traces untrusted-input dataflows from source to sink across functions and files — exactly the shape of a Zip Slip bug, where the source is an archive entry name and the sink is a file-path constructor or write call with no intervening canonicalization check. Each finding carries the full source→sink dataflow trace along with CWE-22 mapping and severity, so a developer sees precisely which extraction loop is missing the destination-prefix check rather than a bare "possible path traversal" warning on a line number. Because SAST findings share Safeguard's unified findings model with SCA and reachability data, a vulnerable archive-handling dependency (like the pre-1.13 zt-zip affected by CVE-2018-1002201) and a first-party Zip Slip pattern in your own extraction code get prioritized together instead of triaged as two unrelated tickets.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.