ASVS 5.0 V5.3.3 (level 3) requires that server side file processing, such as decompression, ignores user provided path information, to prevent vulnerabilities like zip slip.
Archive entries carry names, and names can contain paths. An entry called ../../app/config.js is perfectly valid inside a zip. A naive extractor joins that name to its destination folder and writes the file two levels above it.
Why it matters
Zip slip lets an attacker write files to arbitrary places the extracting process can reach: overwrite application code, drop a script into a web root, replace a cron file or a config. In many stacks that is remote code execution through nothing more than a file upload. Several widely used libraries across languages shipped this bug in the past, which shows how easy it is to write.
It differs from V5.3.2: here the hostile path is not in a form field, it is hidden inside the file you decided to trust enough to unpack. It also complements V5.2.5, which handles link entries, and V5.2.3, which handles size.
How to implement it
Two safe designs, in order of preference.
1. Ignore entry names completely. Write each entry to a name you generate, and keep the original only as metadata. This works when you only need the contents, not the folder layout.
import uuid, zipfile
from pathlib import Path
def extract_flat(archive, dest: Path):
dest.mkdir(parents=True, exist_ok=True)
out = []
with zipfile.ZipFile(archive) as z:
for info in z.infolist():
if info.is_dir():
continue
target = dest / uuid.uuid4().hex # no user path used
with z.open(info) as src, open(target, 'xb') as dst:
dst.write(src.read(MAX_ENTRY + 1)[:MAX_ENTRY])
out.append((info.filename, target))
return out
2. If layout matters, resolve and verify every entry.
Path base = destDir.toRealPath();
try (ZipFile zf = new ZipFile(archive)) {
for (ZipEntry e : Collections.list(zf.entries())) {
Path target = base.resolve(e.getName()).normalize();
if (!target.startsWith(base)) {
throw new SecurityException("blocked entry: " + e.getName());
}
if (e.isDirectory()) { Files.createDirectories(target); continue; }
Files.createDirectories(target.getParent());
try (InputStream in = zf.getInputStream(e)) {
Files.copy(in, target); // fails if it already exists
}
}
}
The check must happen on the normalized, absolute result, before any write. Also reject absolute entry names, drive letters, backslashes and null bytes, refuse to overwrite existing files, and extract as a low privilege user into a scratch folder. Combine with the size and symlink checks from the neighboring requirements. Keep archive libraries up to date, since many zip slip fixes arrive as library patches, and a dependency scan will show you which of your versions are affected.
Language specific notes and hardening
Know how your platform behaves rather than assuming it is safe.
- Python.
zipfile.extractstrips leading slashes and..components in the member name, but behavior has varied across versions and platforms, andtarfile.extractallhistorically did not. Use thefilter='data'option for tar on versions that support it, and keep your own check as well. - Java.
ZipEntry.getName()returns the raw name. Always normalize and compare with the destination, as shown above. - Node. Several popular unzip packages were affected by zip slip in the past. Prefer maintained libraries, and wrap extraction with your own containment check.
- Go.
archive/zipreturns names untouched, and recent versions expose a check throughzip.ErrInsecurePathwhenGODEBUGsettings are enabled. Do not rely on it. Usefilepath.IsLocalon each name and join under a rooted path.
for _, f := range r.File {
if !filepath.IsLocal(f.Name) {
return fmt.Errorf("blocked entry %q", f.Name)
}
target := filepath.Join(dest, f.Name)
// create parents, open with O_EXCL so existing files are never overwritten
}
Add structural limits at the same time: maximum entries, maximum depth of path segments, and maximum name length. These stop deep directory tricks and names that cause trouble on some filesystems.
Operationally, extract in a throwaway location, validate the result, then move the accepted files to final storage under generated names. If anything fails, delete the whole scratch folder. Never extract directly into a directory that holds code, templates or configuration. Log each blocked entry with the archive hash, so repeat offenders are visible, and keep the extraction library on your dependency watch list.
How to test it
Create a malicious archive and check it cannot write outside the target.
import zipfile
with zipfile.ZipFile('slip.zip', 'w') as z:
z.writestr('../../escape.txt', 'pwned')
z.writestr('/tmp/abs.txt', 'pwned')
z.writestr('..\\..\\win.txt', 'pwned')
z.writestr('ok/readme.txt', 'fine')
Process it through your real upload path. Afterward, search the filesystem for the marker files:
find / -name escape.txt -o -name abs.txt -o -name win.txt 2>/dev/null
Nothing should be found, and the request should fail or skip the bad entries by design. Repeat with tar archives, and run the same test in CI. Finally, run a dependency scan on your project, such as the free in browser scan at gold.safeguard.sh/scan, to see whether you use an extraction library version with a known zip slip advisory.
Common mistakes
- Concatenating the destination and the entry name without normalizing.
- Checking for
..as a substring, which misses encoded and platform specific forms. - Verifying the path after writing the file.
- Following existing symlinks in the destination folder.
- Using an old archive library with a known zip slip flaw.
- Extracting as a privileged user into a directory that contains application code.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
What makes an archive entry dangerous?
Its name can contain path segments such as ../ that, if joined to the destination, place the file outside the intended folder.
When should the containment check run?
Before writing, on the normalized absolute target path, and the write should refuse to overwrite existing files.
Sources
- OWASP Application Security Verification Standard, requirement V5.3.3 (level 3)
- OWASP File Upload Cheat Sheet
- Safeguard free in-browser dependency scan