Python tarfile extractall Path Traversal (CVE-2007-4559): How It Happens and How to Fix It
Before extraction filters, tarfile.extractall() wrote each member to the path stored in the archive, so names with .., absolute paths or links could place files outside the target folder. How the filter="data" argument from PEP 706 blocks this, why the destination must not come from user input, and which size limits to add, with a Python example.
Plain English Explainer (ELI5)
A removals company unpacks boxes into your new flat and puts each item wherever its label says. A label that reads 'kitchen, then out of the front door, then into the neighbour's bedroom' gets followed to the letter, because the packers only read labels and never check that the address is still inside your flat. An archive works the same way: each file carries its own destination path. The fix is a foreman who reads every label first and refuses anything that would leave the flat, along with anything that is not an ordinary box, such as a sign pointing somewhere else.
Core Concepts & Subsystem Terms
- CVE-2007-4559
- Python's
tarfilemodule did not stop member names from leaving the extraction directory. The issue was known from 2007, and in 2022 Trellix estimated that roughly 350,000 open-source projects calledextractall()on untrusted archives. - Member names with .. or absolute paths
- A tar entry named
../outside.txtor/etc/cron.d/jobresolves outside the destination when it is joined to it. Without a filter,extractall()writes to that resolved path with the process's permissions. - Symbolic and hard link members
- An archive can first create a link inside the destination that points elsewhere, then write a regular file through it. Checking names alone misses this, which is why filters also inspect link targets.
- Extraction filters (PEP 706)
filter="data"rejects absolute names,..escapes, links that leave the destination and device files, and clears set-user-ID and executable bits for others. It is available from Python 3.12 and in the 3.8 to 3.11 security releases, and it becomes the default in Python 3.14. This mitigates known cases but does not make extraction a sandbox; later CVEs, including CVE-2026-82049, bypassed filters on unpatched CPython.- Resource limits
- Filters do not limit size. A small compressed archive can expand into millions of members or many gigabytes, so count members and total size before extracting.
Step-by-Step Attack Flow
The app accepts an uploaded archive
A data import, plugin installer or CI artifact handler receives a .tar.gz from a user.
A member name points outside the folder
One entry's stored path climbs out of the destination or is absolute. A test archive with a member named ../outside.txt illustrates the case.
extractall writes to the resolved path
The file lands wherever the name resolves, and the destination itself may come from a user-supplied job_id.
Application files are replaced
Overwritten configuration, templates or scripts run the next time the application loads them, which is how this path traversal becomes code execution.
Source Code: Flaw vs. Secure Implementation
# imports.py: every member goes wherever its stored name points
import tarfile
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
# caller-chosen folder, no filter, no size limits
def import_bundle(archive_path: str, job_id: str) -> Path:
dest = UPLOAD_ROOT / job_id
dest.mkdir(parents=True, exist_ok=True)
with tarfile.open(archive_path) as tar:
tar.extractall(dest)
return dest
# imports.py: data filter, server-named folder and size limits
import tarfile
import uuid
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
MAX_MEMBERS = 10_000
MAX_TOTAL_BYTES = 500 * 1024 * 1024
# reject archives with too many members or too many bytes
def check_limits(tar: tarfile.TarFile) -> None:
members = tar.getmembers()
if len(members) > MAX_MEMBERS or sum(m.size for m in members) > MAX_TOTAL_BYTES:
raise ValueError("archive too large")
# filter='data' blocks .., absolute names, outside links and device files
def import_bundle(archive_path: str) -> Path:
dest = UPLOAD_ROOT / uuid.uuid4().hex
dest.mkdir(parents=True)
with tarfile.open(archive_path, mode="r:*") as tar:
check_limits(tar)
tar.extractall(dest, filter="data")
return dest
Recent tarfile filter bypasses
PEP 706 filters reduce path and link risks, but they are not a sandbox and must be kept patched. CVE-2026-82049 showed that a crafted hard link to a symlink could change permissions or modification time of a file outside the destination, or expose its contents inside the extracted tree on affected CPython through 3.13. CVE-2026-19672 let crafted member names create empty directories outside the destination on POSIX. Update supported CPython branches to 3.11.17, 3.12.15, 3.13.16, or later; Python 3.14 is not affected by CVE-2026-82049, and 3.14.8 includes the CVE-2026-19672 fix. Python 3.10.22 includes fixes but 3.10 is end-of-life. Keep filter="data", extract into a fresh server-named directory, apply resource limits, and run extraction with least privilege.
Engineering & System Hardening Checklist
- Pass
filter="data"to everyextractall()andextract()call, and fail CI when Bandit B202 or a Semgrep rule finds one withoutfilter=. - Extract into a fresh directory named by the server, such as a UUID, never into a path built from request data.
- Check the member count and total uncompressed size before extracting, and reject archives above the limits.
- Run the extraction worker as a user with no write access to application code or configuration.
- Add a CI test with an archive holding
../outside.txtand an absolute-path member, and expecttarfile.FilterError. - Upgrade CPython to a patched release. Upstream fixes include 3.11.17, 3.12.15, and 3.13.16; Python 3.10 is end-of-life. Check your vendor's backports.
Sources
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- NVD — CVE-2007-4559 (Python tarfile)
- PEP 706 — Filter for tarfile.extractall
- Python docs — tarfile extraction filters
- Python Security Advisory — CVE-2026-82049
- Python Security Advisory — CVE-2026-19672
- Python Insider — October 2026 security releases
- Python 3.14.8 release notes — CVE-2026-82049 not affected