Lion Cipher Lab
Defeat Zip Slip: stop path traversal in archive extraction (from scratch, offline)
- AppSec / DevSecOps
- Advanced
- ~75 min
- Free · no account required
Why it matters
Almost every non-trivial service unzips something a user gave it: CI systems unpack build artifacts, plugin and theme installers unpack uploads, avatar and 'import my data' features unpack archives, container builders unpack layers. The moment code takes a file NAME out of an untrusted archive and joins it to a destination directory, it inherits a thirty-year-old bug that Snyk repackaged and named 'Zip Slip' in 2018: an archive entry called ../../../../etc/cron.d/pwned does not extract into your upload directory, it extracts OVER a file somewhere else on disk, because…
What you'll learn
- Crafting a malicious archive from scratch with Python's zipfile and tarfile: benign entries next to weaponized ones carrying ../ traversal, an absolute path, a Windows backslash variant, and (tar-only) a symlink and hardlink that point outside the extraction tree
- Reproducing the Zip Slip bug in the naive extractor -- os.path.join(dest, member_name) with no containment check -- and MEASURING the escape with os.path.exists rather than asserting it
- Explaining precisely why os.path.join(dest, '../../etc/x') collapses out of dest, and why an absolute member name discards dest entirely
- Writing the canonical safe extractor from scratch: resolve with os.path.realpath, require os.path.commonpath([dest, resolved]) == dest, reject absolute member names, and refuse symlink/hardlink members whose target escapes
- Knowing what the standard library does and does NOT protect you from: zipfile.extractall sanitizes ../ since Python 3.6.2, but tarfile was unsafe by default for over a decade (CVE-2007-4559) until the filter= argument (PEP 706) arrived in 3.12 and became a safe default in 3.14
- Naming the sibling risks -- zip bombs / decompression amplification -- and the do-it-better controls: extract to a scratch dir under a size and entry-count quota, drop privileges, and use tarfile filter='data' on 3.12+
Steps in this lab
- The vulnerability in one sentence, and why it still ships in 2026
- Set up: one folder, one file, and the safety promise
- Build the malicious ZIP from scratch, and the cross-platform backslash gotcha
- Build the malicious TAR: the symlink and hardlink escalation ZIP cannot do
- The safe extractor from scratch: realpath + commonpath containment
- The naive extractor: why os.path.join(dest, name) is the entire bug
- Does the standard library save you? zipfile vs tarfile, and the decade-long gap
- Run it: measure the escape, then watch containment block every variant
- The judgment layer: when this bites, and how to do extraction better
- When the standard library protects you, when it still does not, and the limits of this engine