FlawOpen
● CWE-22 · CVE-2007-4559

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 tarfile module 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 called extractall() on untrusted archives.
Member names with .. or absolute paths
A tar entry named ../outside.txt or /etc/cron.d/job resolves 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

Step 1

The app accepts an uploaded archive

A data import, plugin installer or CI artifact handler receives a .tar.gz from a user.

Step 2

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.

Step 3

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.

Step 4

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

UNPATCHED FLAW
# 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
HARDENED SECURE PATCH
# 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

Sources

← Browse the full security directory All vulnerability guides →