
Un POC para la vulnerabilidad de evasión de la lista blanca de Path Traversal en Apache Livy.
Solo con fines educativos y de investigación en seguridad. No lo utilice contra sistemas que no sean de su propiedad o para los que no tenga permiso explícito por escrito. → Descargo de responsabilidad completo
| Campo | Detalle |
|---|---|
| ID CVE | CVE-2025-66249 |
| Severidad | Importante (CVSS N/A — evaluación del NVD pendiente al 2026-03-15) |
| Afecta | Apache Livy 0.3.0-incubating hasta 0.8.0-incubating — solo cuando livy.file.local-dir-whitelist está configurado con un valor distinto del predeterminado |
| Corregido en | Apache Livy 0.9.0-incubating |
| CWE | CWE-22: Limitación incorrecta de una ruta a un directorio restringido ('Path Traversal') |
| Divulgado | 2026-03-12 (OSS-Sec) / 2026-03-13 (NVD) |
| Reportado por | Hiroki Egawa (descubridor) |
Un usuario autenticado con acceso a la interfaz REST o JDBC de Livy puede enviar una sesión de Spark o un trabajo por lotes con un valor de configuración de ruta de archivo diseñado para escapar de la lista blanca de directorios permitidos.
Causa raíz — Omisión de path traversal en la comprobación de la lista blanca (Session.scala)
Cuando livy.file.local-dir-whitelist está configurado, Livy 0.8.0 valida las rutas
enviadas llamando a String.startsWith() de Java sobre la ruta cruda y sin normalizar.
Esta comprobación se puede omitir usando secuencias de traversal ../:
/opt/safe-data/../sensitive/secret.txt
La cadena cruda comienza con /opt/safe-data, por lo que la comprobación se supera — pero la ruta
se resuelve a /opt/sensitive/secret.txt, que está completamente fuera del directorio
incluido en la lista blanca.
Condición de activación: La vulnerabilidad solo se puede explotar cuando
livy.file.local-dir-whitelist está configurado con un valor distinto del predeterminado (no vacío).
Si la lista blanca está vacía (el valor predeterminado), la validación de rutas se omite por completo y
el problema no se manifiesta.
Impacto: Un atacante que envía una sesión a través de la API REST de Livy puede hacer referencia a archivos locales arbitrarios en el host del servidor Livy. En un clúster de analítica compartido, esto se traduce en una posible exposición de credenciales, claves, archivos de configuración o cualquier dato legible por el usuario del proceso de Livy.
Session.scalaVulnerable (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Corregido (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Ambas versiones se clonaron directamente desde el repositorio oficial de Apache Livy en GitHub usando los siguientes comandos exactos:
Repositorio: https://github.com/apache/incubator-livy
# Versión vulnerable — clonada en ./livy-0.8.0/
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# Versión corregida — clonada en ./livy-0.9.0/
git clone --depth=1 --branch v0.9.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.9.0
| Versión | Etiqueta (tag) | Commit resuelto | Ruta local |
|---|---|---|---|
| 0.8.0-incubating | v0.8.0-incubating | 78b512658e4baf1183f2b352203ada1928d8111a | ./livy-0.8.0/ |
| 0.9.0-incubating | v0.9.0-incubating | 7215f209b25b96488189567807eaded00953a492 | ./livy-0.9.0/ |
Session.scala: Paths.get().normalize() antes de la comprobación de la lista blanca import java.io.InputStream
import java.net.{URI, URISyntaxException}
+import java.nio.file.Paths
import java.security.PrivilegedExceptionAction
+import java.util.concurrent.{Executors, LinkedBlockingQueue, ThreadFactory, ThreadPoolExecutor, TimeUnit}
import java.util.UUID
...
if (resolved.getScheme() == "file") {
// Make sure the location is whitelisted before allowing local files to be added.
- require(livyConf.localFsWhitelist.find(resolved.getPath().startsWith).isDefined,
+ require(livyConf.localFsWhitelist.find(
+ Paths.get(resolved.getPath()).normalize.startsWith).isDefined,
s"Local path ${uri.getPath()} cannot be added to user sessions.")
}
Impacto en v0.8.0:
La comprobación startsWith sobre la cadena cruda se puede omitir con una carga útil de path traversal.
Ejemplo: si livy.file.local-dir-whitelist = /opt/safe-data
/opt/safe-data/../sensitive/secret.txt
"/opt/safe-data/../sensitive/secret.txt".startsWith("/opt/safe-data") → true (omitida)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → false (bloqueada)Los diffs se generaron clonando ambas etiquetas localmente (ver arriba) y ejecutando:
diff -u \
livy-0.8.0/server/src/main/scala/org/apache/livy/sessions/Session.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/sessions/Session.scala
Atacante (usuario autenticado REST/JDBC)
│
▼
POST /sessions
{
"conf": {
"spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
← la ruta comienza con el prefijo de la lista blanca — String.startsWith() se supera
← pero se resuelve FUERA del directorio mediante traversal ../
}
}
│
▼
Livy 0.8.0 — comprobación de lista blanca omitida (startsWith crudo, sin normalización)
│
▼
Spark lee el archivo y lo distribuye a los ejecutores
│
▼
El atacante recupera el contenido del archivo mediante la salida del trabajo / los registros
Todos los pasos de este PoC se ejecutaron y validaron en el siguiente sistema:
| Componente | Detalle |
|---|---|
| SO del host | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Arquitectura | x86_64 |
| Memoria total | 15 GiB |
| Motor Docker | 28.2.2 |
| JDK del host | OpenJDK 17.0.18 (usado solo por el host — los contenedores usan eclipse-temurin:11-jdk-focal) |
| Imagen base del contenedor | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| Versión de Spark (ambas imágenes) | 3.1.3 con Hadoop 3.2 |
| Versión de Livy — imagen vulnerable | 0.8.0-incubating |
| Versión de Livy — imagen corregida | 0.9.0-incubating |
CVE-2025-66249-POC/
├── docker/
│ ├── fixed/
│ │ ├── Dockerfile
│ │ ├── livy.conf
│ │ └── start.sh
│ └── vulnerable/
│ ├── Dockerfile
│ ├── livy.conf
│ └── start.sh
├── test/
│ └── validate.sh
├── .gitignore
├── LICENSE
└── README.md