flawopen.com/Path Traversal/Zip Slip
What is a Zip Slip vulnerability?
A zip file's internal file list can include entries named things like ../../etc/cron.d/evil. If the code extracting the archive trusts those names blindly, "unzipping a file" can secretly write files anywhere on disk the process has permission to reach — the archive extraction itself is a path traversal vector.
Why it's easy to miss
Archive extraction feels like a solved, boring operation — most developers don't think of "unzip this file" as a place untrusted input reaches the filesystem. But every entry name inside a zip (or tar) archive is attacker-controlled if the archive itself came from an untrusted source (a user upload, a downloaded dependency), and naive extraction code that does outputDir + entry.name without validation is exactly the path-traversal pattern, just triggered by archive contents instead of a URL parameter.
for entry in zip_file:
dest = os.path.join(output_dir, entry.name)
write_file(dest, entry.read())
# entry.name could be "../../../etc/cron.d/x"
root = os.path.realpath(output_dir)
os.makedirs(root, exist_ok=True)
for entry in zip_file:
dest = os.path.realpath(
os.path.join(root, entry.name)
)
try:
inside_root = os.path.commonpath([root, dest]) == root
except ValueError:
inside_root = False
if not inside_root:
raise ValueError("Unsafe entry path")
write_file(dest, entry.read())
This containment check prevents lexical path escapes, including sibling names that share a string prefix. Extract into a newly created, private directory; reject symlink entries and account for pre-existing symlinks or concurrent filesystem changes, which require race-safe filesystem APIs or a maintained extraction library.
FAQ
Python archive APIs have different path protections
The code pair above is a conceptual custom extraction loop; it is not Python’s built-in ZipFile.extractall(). Python documents that ZipFile.extract() and extractall() attempt to prevent absolute-path and .. escapes, but still warns you to inspect untrusted archives. zipfile.Path does not sanitize member names, so callers must validate them. For TAR archives, extraction filters arrived in Python 3.12; Python 3.14 changed the default to filter="data". Set that filter explicitly on supported runtimes where the default may be less restrictive. Even the data filter does not block every unsafe behavior or resource-exhaustion risk. Extract in a fresh private staging directory and enforce limits on archive size, member count, total expanded bytes, and processing time.
Some newer library versions added protections after this pattern was widely publicized in 2018, but many still don't validate by default — check your specific library's documentation rather than assuming, and validate explicitly regardless.