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 never designed to be a security boundary.
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