A user uploads a ZIP file. The server extracts it into a temporary directory. Nothing seems wrong. The files are written. The process completes cleanly.
But one of those files didn't land in the temporary directory. It landed somewhere else on the filesystem entirely. Somewhere it shouldn't have reached.
The ZIP file didn't execute any code. No memory was corrupted. The extraction library worked exactly as designed. The problem was that the application trusted a filename inside the archive to be safe, and used it directly to construct a filesystem path.
That's Zip Slip.
How ZIP Archives Store File Paths
A ZIP archive is not just a collection of compressed file contents. Each entry in the archive has metadata: a filename, compression method, timestamps, and other attributes. That filename can contain a path, not just a bare name.
When you create a ZIP containing reports/2024/summary.csv, the archive entry stores reports/2024/summary.csv as the entry name. Extraction software reads this name and uses it to reconstruct the directory structure at the destination.
The critical point is that the archive itself defines the filename. The extraction software reads whatever string is stored in the entry metadata and uses it as a filesystem path. If that string contains ../ sequences, the path can escape the intended extraction directory.
How Path Traversal Works in This Context
../ is a standard filesystem path component meaning "go up one directory level." It is not a special attack payload. It is valid in any operating system's path resolution rules.
So if an archive entry's name is ../important.conf, and the extraction directory is /tmp/upload/, the extracted destination becomes:
Extraction directory: /tmp/upload/
Entry name: ../important.conf
Joined path: /tmp/upload/../important.conf
Resolved path: /tmp/important.conf
The file lands one level above the extraction directory. Extend this further:
Entry name: ../../etc/cron.d/job
Joined path: /tmp/upload/../../etc/cron.d/job
Resolved path: /etc/cron.d/job
Each ../ walks up one level. Enough of them can reach anywhere on the filesystem from the extraction root.
The normal flow versus the vulnerable flow:
Safe archive entry
↓
Extraction directory + entry name
↓
Resolved path inside extraction directory
↓
File written safely
Malicious archive entry (../../etc/cron.d/job)
↓
Extraction directory + entry name
↓
Resolved path escapes extraction directory
↓
File written to arbitrary location
Why Checking the Filename Alone Is Not Enough
A naive defense might look for ../ in the entry name and reject any entry that contains it. That catches the obvious case. But it doesn't catch everything.
URL encoding and other representations. Some extraction code may decode entry names before writing. An entry named ..%2F or %2e%2e/ may be decoded into ../ after any string-level check has already run. The check sees a safe string; the filesystem sees a traversal.
Absolute paths. An archive entry can also store an absolute path like /etc/passwd rather than a relative one. If the extraction code joins the extraction directory with an absolute path on some platforms, or simply uses the path as-is, the result skips the extraction directory entirely.
Mixed separators. On Windows, both / and \ are valid path separators. An entry named ..\config\app.ini may pass a check that only looks for ../ while still traversing upward.
Normalization after joining. The important check is not whether the entry name looks suspicious. The important check is whether the fully resolved destination path still falls inside the intended extraction directory. These are different questions.
The Role of Path Canonicalization
Canonicalization is the process of resolving a path to its definitive form: expanding .. components, resolving symlinks, collapsing redundant separators, and producing an absolute path.
The correct approach is:
entry_name = read from archive
destination = extraction_dir + entry_name
canonical = canonicalize(destination)
if not canonical.startswith(extraction_dir):
reject entry
This check happens after resolution, not before. It doesn't matter what the entry name looks like. What matters is where the file would actually end up.
The naive approach that fails:
entry_name = read from archive
if ".." in entry_name:
reject
else:
write to extraction_dir + entry_name # still vulnerable
Checking the entry name in isolation cannot account for how the operating system will interpret the joined path. The filesystem is the authority on where a path points. Any security check that doesn't consult the filesystem's own resolution is working with incomplete information.
Symlinks Complicate Extraction Further
Zip Slip becomes more complex when the archive contains symbolic links rather than regular files.
A symlink is a filesystem object that points to another path. If an archive contains an entry that creates a symlink at extract/link pointing to /etc, and then a second entry that writes a file to extract/link/cron.d/job, the second write follows the symlink. The file ends up at /etc/cron.d/job.
Step 1: Extract symlink
archive entry "link" → symlink to /etc
written to /tmp/upload/link → points to /etc
Step 2: Extract file using symlink path
archive entry "link/cron.d/job"
resolved: /tmp/upload/link/cron.d/job
followed: /etc/cron.d/job
Both entries individually may pass a path traversal check. The first creates a symlink inside the extraction directory. The second writes a file to a path inside the extraction directory. But the combination results in a write outside it.
Safe extraction requires either rejecting symlinks from untrusted archives, or resolving intermediate symlinks before checking whether the final destination is inside the intended extraction root.
Where Zip Slip Appears
The vulnerability isn't specific to ZIP. Any archive format that stores filenames with paths can carry the same problem: tar, jar, war, ear, apk, gem. The extraction logic, not the format, determines the exposure.
Web applications that accept archive uploads are the most obvious case. A user uploads a ZIP, the server extracts it, and if the extraction code doesn't validate entry paths, the archive controls where files land.
CI/CD pipelines and build systems unpack artifacts and dependencies regularly. Package managers in several ecosystems have historically been affected. Backup and restore systems often extract with elevated privileges, which increases the reach of any malicious entries. Any automated pipeline that accepts archives from external sources and extracts them programmatically is in scope.
Impact Depends on What the Process Can Reach
Zip Slip enables arbitrary file write within what the extraction process can access. A web application running as a low-privilege user has limited reach. A build pipeline with broader system access might allow overwriting configuration files, startup scripts, or application code. An extraction process running as root can reach anything writable on the filesystem.
The vulnerability enables writing, not reading, and does not by itself provide code execution. But overwriting a configuration file, an application startup script, or a cron job can be a path to execution depending on the system. Whether an existing file can be overwritten also depends on filesystem permissions and other constraints. The impact is contextual.
Zip Slip vs Ordinary Path Traversal
Classic path traversal attacks manipulate URL parameters or form fields to cause a server to read a file outside its intended scope: GET /files?name=../../etc/passwd.
Zip Slip is a path traversal in archive extraction. The traversal string doesn't come from a URL parameter or an HTTP header. It comes from the filename stored inside an archive entry.
The ZIP format itself is not executing anything and is not inherently malicious. The vulnerability is in the extraction logic that treats the archive-controlled filename as a safe basis for constructing a filesystem path. The archive is just the delivery mechanism for a filename the application should not have trusted.
Defenses
Resolve before checking. Compute the canonical absolute path of the destination before writing any file. Verify that it starts with the canonical absolute path of the extraction directory. Reject any entry whose resolved destination falls outside.
Handle symlinks explicitly. Decide whether your extraction logic should allow symlinks at all. If symlinks are necessary, resolve intermediate symlinks during path verification, not just the final path.
Use a well-maintained extraction library. Many languages and ecosystems now have extraction libraries that perform these checks by default. Prefer those over manual path construction.
Do not rely on entry name inspection. Checking whether an entry name "looks safe" is insufficient. The resolution happens at the filesystem level, and the check must happen there too.
Treat extraction as a privilege boundary. If the extraction process runs with elevated privileges, the blast radius of a successful Zip Slip is larger. Run extraction with the minimum necessary permissions.
Test with adversarial archives. A test suite that only extracts well-formed archives will not catch this. Explicitly test with entries containing ../ sequences, absolute paths, null bytes in filenames, and symlinks pointing outside the extraction root.
The Technical Takeaway
Archive filenames are attacker-controlled input. An archive produced by a user, a third-party system, or an external package source defines its own entry names, and those names can contain any string the format allows.
The code that converts those names into filesystem paths is the security boundary. Not the archive format. Not the filename extension. Not a string check on the entry name. The security boundary is the moment the application decides where to write the file, and whether it verified that decision against the filesystem's own resolution before acting on it.
A ZIP file that writes outside its extraction folder is not doing anything the ZIP format prohibits. It is doing exactly what the extraction code allows.
Top comments (0)