Self-contained Docker lab demonstrating CVE-2007-4559 (TarSlip) directory traversal via Python's tarfile module. Includes vulnerable and fixed APIs, payload generation, and an interactive demo script for security education.
Educational use only. This lab intentionally exploits file system vulnerabilities inside isolated Docker containers. Do not run on any system with sensitive data or in production environments.
A self-contained Docker lab that demonstrates CVE-2007-4559 — the infamous "TarSlip" vulnerability in Python's tarfile module — through a concrete, end-to-end attack chain:
extractall() blindly writes a tar entry named ../../../etc/passwd outside the extraction directory, overwriting the real system file./admin endpoint.Python's tarfile.extractall() faithfully reproduces every entry in a tar archive, including entries whose names contain path traversal sequences. It was .
../tar entry name : ../../../etc/passwd
extraction dir : /shared/uploads/a1b2c3d4/
resolved path : /shared/uploads/a1b2c3d4/../../../etc/passwd
= /etc/passwd ← system file overwritten
| Timeline | |
|---|---|
| 2007 | Bug reported to the Python security team |
| 2007 – 2022 | Marked "not a security issue" — tarfile is "working as intended" |
| 2022 | Trellix researchers scan GitHub and find 350,000+ repos calling extractall() on untrusted input |
| 2022 | Public disclosure. CVE-2007-4559 resurfaces. Industry-wide scramble. |
| 2023 | PEP 706 ships filter='data' in Python 3.12 — the fix is a single argument |
The demo runs in three beats, each requiring a keypress to advance.
GET /admin → 401. The admin endpoint exists and is protected. The attacker does not know the password.innocent.tar.gz → files land inside the sandbox directory. Everything looks normal.Attacker crafts tarslip_passwd.tar.gz
└─ entry: "../../../etc/passwd"
content: admin:hacked:1001:... ← planted password
│
▼
POST /upload (multipart file upload)
│
▼
extractall("/shared/uploads/{uuid}/")
resolves "../../../etc/passwd" → /etc/passwd ← CVE-2007-4559
│
▼
GET /admin Authorization: Basic admin:hacked
│
▼
HTTP 200 — "Welcome, admin! You have full admin access."
flag: CVE-2007-4559{tarslip_passwd_overwrite_to_admin_rce}
One HTTP POST. No shell. No RCE payload. Just a tar file.
The same tarball is uploaded to the fixed API, which passes filter='data' to extractall(). Python raises tarfile.OutsideDestinationError — the traversal is blocked, /etc/passwd is untouched, and /admin stays locked.
# Vulnerable — default before Python 3.14
tar.extractall(extraction_dir)
# Fixed — PEP 706 (Python 3.12+)
tar.extractall(extraction_dir, filter='data')
One argument. Fifteen years to ship.
Four services on an isolated Docker bridge network (tarslip-net). Nothing reaches the internet.
┌─────────────────────────────────────────────────────────┐
│ tarslip-net (bridge) │
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ vulnerable-api │ │ file-server │ │
│ │ python:3.11.3 │ │ nginx:alpine │ │
│ │ port 8000 │ │ port 8080 (host) │ │
│ │ │ │ │ │
│ │ POST /upload │ │ Serves /shared over │ │
│ │ GET /admin │ │ HTTP — browse extracts │ │
│ │ GET /health │ │ visually │ │
│ └────────┬────────┘ └────────────┬─────────────┘ │
│ │ shared-storage volume │ │
│ └────────────────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ attacker │ │
│ │ python:3.12 │ (no host port — internal only) │
│ │ │ │
│ │ craft_malicious.py — generates tarballs │
│ │ demo.py — drives the demo │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
| Service | Image | Role | Host port |
|---|---|---|---|
vulnerable-api | python:3.11.3-slim | Flask upload API + /admin guarded by /etc/passwd auth | 8000 |
fixed-api | python:3.12-slim | Same code + USE_SAFE_EXTRACTION=true | 8000 |
file-server | nginx:alpine | Directory listing of extracted files | 8080 |
attacker | python:3.12-slim | Payload generator + demo driver | — |
The vulnerable and fixed APIs use identical source code. The only difference is the USE_SAFE_EXTRACTION=true environment variable on the fixed container, which flips the single filter='data' argument.
CVE-2007-4559-lab/
├── run_demo.sh ← start here
├── docker-compose.vulnerable.yml
├── docker-compose.fixed.yml
├── vulnerable-api/
│ ├── app.py # Flask API: /upload + /admin + /health
│ ├── Dockerfile # seeds admin:s3cr3t_Adm1nPass into /etc/passwd
│ └── requirements.txt
├── file-server/
│ ├── Dockerfile
│ └── nginx.conf
└── attacker/
├── craft_malicious.py # generates innocent.tar.gz + tarslip_passwd.tar.gz
├── demo.py # four-mode CLI driver (craft/baseline/exploit/verify)
├── Dockerfile
└── requirements.txt
docker compose version)bashgit clone https://github.com/your-username/CVE-2007-4559-lab.git
cd CVE-2007-4559-lab
bash run_demo.sh
The script is fully interactive. It prints a narrative before each step and waits for Enter to advance. No prior Docker knowledge is needed to follow along.
| Pause | Narrative shown | Action on Enter |
|---|---|---|
| 1 | CVE timeline, what the three beats are | Build vulnerable stack |
| 2 | Container roles, seeded admin password | Generate payloads |
| 3 | What's inside each tarball | Beat 1 — baseline |
| 4 | Why /admin is 401, what normal extraction looks like | Beat 2 — exploit |
| 5 | The exact traversal math, what gets overwritten | Swap to fixed stack |
| 6 | What filter='data' does and why it works | Beat 3 — verify |
| 7 | Key takeaways + broader ZipSlip pattern | Teardown |
If you want to step through manually:
# Vulnerable stack
docker compose -f docker-compose.vulnerable.yml up --build -d
docker compose -f docker-compose.vulnerable.yml exec attacker python craft_malicious.py
docker compose -f docker-compose.vulnerable.yml exec attacker python demo.py baseline
docker compose -f docker-compose.vulnerable.yml exec attacker python demo.py exploit
# Fixed stack
docker compose -f docker-compose.vulnerable.yml down
docker compose -f docker-compose.fixed.yml up --build -d
docker compose -f docker-compose.fixed.yml exec attacker python craft_malicious.py
docker compose -f docker-compose.fixed.yml exec attacker python demo.py verify
# Teardown
docker compose -f docker-compose.fixed.yml down
While the vulnerable stack is running, open http://localhost:8080/uploads/ to browse the extracted session directories in your browser.
/admin endpoint worksThe API seeds a secret admin password into /etc/passwd at image build time:
admin:s3cr3t_Adm1nPass:1001:1001:Administrator:/home/admin:/bin/bash
GET /admin reads this file and checks the second field (password) against the HTTP Basic Auth credentials. The attacker doesn't know s3cr3t_Adm1nPass — but after TarSlip overwrites the file with their own version containing admin:hacked, they do.
This is a simplified model of real-world targets: SSH authorized_keys, application config files, cron jobs, and any credential file the web process can write.
craft_malicious.py uses Python's own tarfile module — the same one that has the bug:
def _add_entry(tar, name, content):
info = tarfile.TarInfo(name=name) # name is the traversal path
info.size = len(content)
tar.addfile(info, io.BytesIO(content))
# Entry name resolves to /etc/passwd when extracted into /shared/uploads/{uuid}/
_add_entry(tar, "../../../etc/passwd", malicious_passwd_content)
No special tools. No binary exploitation. The standard library is both the weapon and the victim.
Python 3.12 introduced filter= in PEP 706. The 'data' filter:
tarfile.OutsideDestinationError on traversal attempts# Before (vulnerable — still the default until Python 3.14)
with tarfile.open(path) as tar:
tar.extractall(dest)
# After (safe)
with tarfile.open(path) as tar:
tar.extractall(dest, filter='data')
For Python 3.11 and earlier, validate manually:
import os
def safe_extract(tar, dest):
dest = os.path.realpath(dest)
for member in tar.getmembers():
member_path = os.path.realpath(os.path.join(dest, member.name))
if not member_path.startswith(dest + os.sep):
raise ValueError(f"Unsafe path: {member.name}")
tar.extractall(dest)
Static analysis: bandit rule B202 flags unsafe extractall() calls in CI.
TarSlip is Python's name for a class of vulnerability that exists in every language with archive extraction APIs:
| Language | Vulnerable API | CVE / Advisory |
|---|---|---|
| Python | tarfile.extractall() | CVE-2007-4559 |
| Java | ZipInputStream | ZipSlip (2018) |
| Go | archive/zip | ZipSlip (2018) |
| .NET | ZipArchive | ZipSlip (2018) |
| Node.js | tar, adm-zip, others | ZipSlip (2018) |
Same root cause everywhere: trusting paths from untrusted archives. Same fix everywhere: canonicalize and validate before writing.
Focus on the before/after code diff and the bandit B202 rule. The goal is "how do we prevent this in our codebase?" — show the PEP 706 migration guide and how to add the check to CI.
Focus on the Trellix disclosure methodology — how they searched GitHub at scale, estimated impact across 350,000 repos, and navigated responsible disclosure for a vulnerability this widespread.
Extend craft_malicious.py to plant an SSH authorized_keys file or a malicious cron entry instead of /etc/passwd. Same technique, different targets — shows that any writable path is an attack surface.
MIT — use freely for education, security research, and conference demos. Do not use the payload generation techniques against systems you do not own.