
Picture a web application where users can upload a .zip file and have its contents extracted on the server. CV upload systems, theme/plugin installers, backup tools, CI/CD pipelines… they all use this pattern: “take the archive, open it, write its contents to disk.”
This is exactly where a vulnerability that’s been known for years — but was brought to wide attention by Snyk’s comprehensive 2018 research — comes into play: Zip Slip.
Zip Slip is a directory traversal and arbitrary file write vulnerability that arises when opening an archive file (zip, tar, jar, war, rar, 7z, apk, cpio…). If a file name inside the archive contains sequences like ../../ and the application writes that name to disk without validating it, an attacker can make the file get written outside the intended folder, anywhere on the system.
Snyk’s 2018 research found this pattern in thousands of projects across many ecosystems, including Java, JavaScript, .NET, Go, and Ruby. The reason is interesting: many languages lack an official, standardized high-level library for extracting archives, so developers have spent years copying the same vulnerable snippet of code from one another (StackOverflow has a big role here). The archive/zip package is no exception — it performs no validation at all and leaves the control entirely up to you.
We initially decided to use Split Query for all our heavy queries. But after a while, we noticed response times were growing significantly on our paginated endpoints — lists returning between 10 and 100 records. Why? Because joining 15 different tables for 10 records (Single Query) is blazing fast, whereas firing 16 separate queries for those same 10 records (Split Query) was causing slowness due to network round-trip latency.
The function below shows the essence of a typical unzip operation:
func unzip(src, destDir string) error {
r, _ := zip.OpenReader(src)
defer r.Close()
for _, f := range r.File {
// VULNERABLE: target path is built without validating f.Name
targetPath := filepath.Join(destDir, f.Name)
outFile, _ := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
rc, _ := f.Open()
io.Copy(outFile, rc)
outFile.Close()
rc.Close()
}
return nil
}
The root of the problem is the line targetPath := filepath.Join(destDir, f.Name). Here, the value of f.Name is read directly from inside the ZIP archive and is entirely under the attacker’s control. If an attacker crafts a file name containing a directory traversal sequence like ../../../etc/cron.d/evil, filepath.Join() will normalize that path — but it does not verify whether the resulting final path still stays under destDir. As a result, the application can end up writing the file to any location outside the target directory, which leads to a classic Zip Slip vulnerability.
The impact varies depending on the target: overwriting a cron file to run a scheduled job, modifying a startup script, adding your own key to the authorized_keys file, or tampering with the application’s own executable files to achieve remote code execution (RCE). What looks like “just a file-copy bug” can often escalate all the way to full server compromise.
Suppose an application extracts user-uploaded ZIP files into the /var/www/app/uploads/themes/ directory and uses a vulnerable unzip()function.
The attacker adds a malicious entry containing a directory traversal sequence, such as ../../../../../tmp/zipslip_poc.txt, alongside a normal style.css file in the archive. Since standard archiving tools typically block names like this, the attacker programmatically generates the ZIP file using a library such as archive/zip. Once the archive is uploaded to the application, if sufficient path validation isn’t performed, the malicious file gets written outside the theme folder.
When unzip() runs on the server, the style.css file is written to the uploads/themes/ directory as expected. However, when the ../../../../../tmp/zipslip_poc.txt entry is processed, filepath.Join()normalizes the path and moves the target to the location /tmp/zipslip_poc.txt. Since the code doesn’t check whether the resulting path goes outside the target directory, it creates the file at this location.
So while the application thinks it’s just uploading a theme, the attacker can write a file to any location the process’s permissions allow. In a real attack, the target might be cron directories, service scripts, or SSH key files instead of /tmp. In that case, the arbitrary file write vulnerability can, under the right conditions, escalate into remote code execution.
This scenario might seem hypothetical, but the exact same pattern was found very recently in a real product. In October 2025, a Zip Slip vulnerability was discovered in Argo Workflows — a popular open-source project under the CNCF umbrella used for workflow orchestration on Kubernetes: CVE-2025-62156 (GHSA-p84v-gxvw-73pf, CVSS 8.1 -High).
The source of the vulnerability is the untar() function in the workflow/executor/executor.go file; the target path is generated with exactly this line:
target := filepath.Join(dest, filepath.Clean(header.Name))
The exact same mistake as in our example above: filepath.Clean simplifies the path but doesn’t prevent it from escaping dest. Argo Workflows can download “artifacts” as tar.gz files for workflow steps and extract them into a specific folder (/work/tmp); researchers demonstrated that an entry name starting with ../ could reach the container’s /etc/ directory via a shared volume mount. The result: critical files like /etc/passwd, /etc/hosts, and /etc/crontab could be overwritten-meaning a bypass of container isolation and potential privilege escalation. The vulnerability affected all versions prior to 3.6.12, as well as the 3.7.0–3.7.2 range; it was fixed in 3.6.12 and 3.7.3.
The fix comes down to a single check: before writing each file, verify that the resulting target path is actually inside the target folder.
destDirAbs, _ := filepath.Abs(destDir)
for _, f := range r.File {
targetPath := filepath.Join(destDirAbs, f.Name)
// SECURITY CHECK: is the target path within destDir’s boundaries?
if !strings.HasPrefix(targetPath, destDirAbs+string(os.PathSeparator)) {
return fmt.Errorf(“zip slip attempt detected: %q”, f.Name)
}
// … the code that writes the file to targetPath stays unchanged
}
The critical point is that filepath.Join() alone is not a security mechanism. This function only normalizes the path; it does not verify that the resulting final path still stays within destDir‘s boundaries. The actual protection comes from the strings.HasPrefix(targetPath, destDirAbs+string(os.PathSeparator)) check, which verifies that the target path starts with destDir. Adding os.PathSeparator here matters too; otherwise, a directory name like /home/app/uploads-evil could be mistakenly accepted as safe just because it starts with /home/app/uploads.
The same logical flaw can also show up in code that opens the following formats:
archive/tar; some sources call this “Tar Slip”Saying “we don’t use zip, this doesn’t concern us” can be misleading. Every piece of code that opens any archive format needs to be checked for this.
zip.OpenReader, tar.NewReader, filepath.Join; check whether a path validation immediately follows them.../../ and observing the behavior is the fastest way to validate.destDir (HasPrefix(target, destDirAbs+separator), not just Contains)..., the target can be escaped via a previously created symlink.../../evil.Zip Slip is a classic example of “a small detail was missed, and a big vulnerability was born.” Trusting the contents of an archive means trusting data under the attacker’s control. filepath.Join gives you “cleanup,” but not a “boundary” — confusing the two is the most common root cause of Zip Slip. The rule is always the same:
Never use unvalidated user input in file system operations.

Subscribe to our cyber security newsletter