
CVE-2025-4138 / CVE-2025-4517 — Python tarfile PATH_MAX Symlink Filter Bypass
Arbitrary file write through filter="data" / filter="tar" extraction
A critical vulnerability in Python's tarfile module allows an attacker to bypass extraction filters ("data" and "tar") and write arbitrary files outside the intended extraction directory. When a privileged process (e.g., a root-level backup script, CI/CD pipeline, or package installer) extracts an attacker-controlled tar archive using the supposedly-safe filter="data" parameter, this exploit achieves full arbitrary file write as that privileged user — typically escalating to root.
The root cause is a behavioral quirk in os.path.realpath(): it silently stops resolving symlinks once the fully-expanded path exceeds PATH_MAX (4096 bytes on Linux, 1024 on macOS). The tarfile filter relies on realpath() for safety checks, but the kernel resolves symlinks independently during extraction — creating a TOCTOU (Time-of-Check-to-Time-of-Use) gap that enables directory escape.
| Field | Value |
|---|---|
| CVE IDs | CVE-2025-4138, CVE-2025-4517 |
| CVSS v3.1 | 9.4 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L |
| CWE | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory |
| Vulnerability Type | Path Traversal via Symlink / Filter Bypass |
| Impact | Arbitrary file write → privilege escalation, sandbox escape, data tampering |
| Attack Vector | Deliver malicious tar archive to any application using tarfile.extractall() with filters |
| Affected | Python 3.12.0 – 3.12.10, 3.13.0 – 3.13.3 |
| Fixed In | Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 |
| Patch | CPython PR #135037 |
| Advisory | GHSA-hgqp-3mmf-7h8f |
| Reporter | Caleb Brown — Google Security Research |
┌───────────────────────────────────────────┐
│ Malicious Tar Structure │
└───────────────────────────────────────────┘
Stage 1 ── Build symlink chain that inflates the resolved path past PATH_MAX
ddd...ddd/ (directory, 247 chars)
a → ddd...ddd (symlink, 1 char name → 247 char dir)
ddd...ddd/ddd...ddd/ (nested directory)
b → ddd...ddd (symlink)
... ×16 levels
Short path (symlinks): a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p ~31 chars
Resolved path (dirs): ddd…/ddd…/ddd…/ddd…/ddd…/ddd…/… ~3968 chars
↑ nearing PATH_MAX
Stage 2 ── Final symlink exceeds PATH_MAX → realpath() stops resolving
a/b/c/…/p/lll…lll → ../../../../../../../../../../../../../../../../..
(16 levels of ".." — traverses back to extraction root)
┌─────────────────────────────────────────────────────────────────┐
│ os.path.realpath() CANNOT expand this → filter says "OK" ✓ │
│ Linux kernel DOES follow chain → actually escapes ✗ │
└─────────────────────────────────────────────────────────────────┘
Stage 3 ── Escape symlink resolves to arbitrary filesystem path
escape → <overflow_link>/../../../../../../../root
Stage 4 ── Create intermediate directories through the escape
escape/.ssh/ (directory, mode 0700 — created by tar extraction)
Stage 5 ── Write payload through the escaped symlink
escape/.ssh/authorized_keys → writes to /root/.ssh/authorized_keys 🔓
| Python Branch | Vulnerable Range | Fixed Version | Status |
|---|---|---|---|
| 3.13 | 3.13.0 – 3.13.3 | 3.13.4 | ✅ Patched |
| 3.12 | 3.12.0 – 3.12.10 | 3.12.11 | ✅ Patched |
| 3.11 | 3.11.4 – 3.11.12 | 3.11.13 | ✅ Patched |
| 3.10 | 3.10.12 – 3.10.17 | 3.10.18 | ✅ Patched |
| 3.9 | 3.9.17 – 3.9.22 | 3.9.23 | ✅ Patched |
| 3.8 | 3.8.17 – 3.8.20 | — | ❌ End of Life |
| 3.14+ | Default filter changed to "data" | Check latest | ⚠️ Higher exposure |
Note: Python 3.14+ changed the default
filterparameter from no filtering to"data", meaning applications that previously had no filter (and were thus already unsafe) now use the vulnerable filter by default.
Any application doing this on a vulnerable Python version is exploitable:
import tarfile
# VULNERABLE — filter="data" can be bypassed
with tarfile.open("untrusted_archive.tar", "r") as tar:
tar.extractall(path="/some/directory", filter="data")
# ALSO VULNERABLE — filter="tar" has the same flaw
with tarfile.open("untrusted_archive.tar", "r") as tar:
tar.extractall(path="/some/directory", filter="tar")