
Un POC para la vulnerabilidad de acceso no autorizado a archivos de Apache Livy.
Solo para fines educativos y de investigación en seguridad. No lo utilice contra sistemas que no posea o para los que no tenga permiso explícito por escrito. → Descargo de responsabilidad completo
| Campo | Detalle |
|---|---|
| CVE ID | CVE-2025-60012 |
| Severidad | Medio (CVSS 6.3) |
| Afectados | Apache Livy 0.7.0-incubating, 0.8.0-incubating — cuando está conectado a Apache Spark 3.1 o posterior |
| Corregido en | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: Validación de entrada incorrecta |
| Divulgado | 2026-03-13 |
| Reportado por | Furue Hideyuki |
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 valores de configuración manipulados. Dos debilidades se combinan para permitir que el atacante haga referencia a archivos del sistema de archivos local fuera de sus rutas permitidas:
Falta de validación para spark.archives — Spark 3.1 introdujo spark.archives como una forma unificada de distribuir archivos comprimidos entre todos los administradores de clúster. La lista codificada de claves de configuración que se validan en las rutas en Livy 0.8.0 (HARDCODED_SPARK_FILE_LISTS) no incluye spark.archives. Por lo tanto, una ruta pasada a través de esta clave nunca se verifica contra la lista blanca del sistema de archivos local (livy.file.local-dir-whitelist), lo que permite a un atacante hacer referencia a cualquier archivo local.
Omisión de path traversal en la verificación de la lista blanca — Incluso para las claves de configuración que SÍ se validan, la comparación de la lista blanca en Livy 0.8.0 utiliza una simple llamada startsWith de Java String sobre la ruta sin procesar. Un atacante puede eludir esto mediante path traversal: /whitelisted/dir/../../etc/passwd pasa la verificación de la cadena pero se resuelve fuera del directorio permitido.
LivyConf.scalaVulnerable (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Corregido (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
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 en este espacio de trabajo utilizando los siguientes comandos exactos:
Repositorio: https://github.com/apache/incubator-livy```bash
git clone --depth=1 --branch v0.8.0-incubating
https://github.com/apache/incubator-livy
livy-0.8.0
git clone --depth=1 --branch v0.9.0-incubating
https://github.com/apache/incubator-livy
livy-0.9.0
| Versión | Etiqueta | 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/` |
## Diferencias exactas de código
Las diferencias se produjeron clonando ambas etiquetas localmente (ver arriba) y ejecutando:```bash
diff -u livy-0.8.0/server/src/main/scala/org/apache/livy/LivyConf.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/LivyConf.scala
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
LivyConf.scala: spark.archives añadido a la lista de archivos codificados```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**Impacto de la entrada faltante en v0.8.0:**
Cuando un usuario envía una sesión con `conf: {"spark.archives": "file:///etc/passwd"}`, Livy
0.8.0 nunca llama a `resolveURIs()` en ese valor y nunca lo compara con
`livy.file.local-dir-whitelist`. La ruta se reenvía a Spark sin validación.
---
### Fix 2 — `Session.scala`: Normalización de ruta antes de la verificación de la lista blanca```diff
def resolveURI(uri: URI, livyConf: LivyConf): URI = {
...
if (resolved.getScheme() == "file") {
- 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 de cadena sin procesar startsWith puede ser evitada con un payload de path traversal.
Ejemplo: si `livy.file.local-dir-whitelist = /opt/safe-data```` /opt/safe-data/../../../etc/passwd
- v0.8.0: `"/opt/safe-data/../../../etc/passwd".startsWith("/opt/safe-data")` → **true** (evadido)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (bloqueado)
## Resumen de vectores de ataque```
Attacker (authenticated REST/JDBC user)
│
▼
POST /sessions (or /batches)
{
"conf": {
"spark.archives": "file:///etc/shadow" ← Attack 1: unvalidated Spark 3.1 key
"spark.jars": "file:///safe/../etc/shadow" ← Attack 2: path traversal bypass
}
}
│
▼
Livy 0.8.0 — validation skipped / bypassed
│
▼
Spark reads the file and distributes it to executors
│
▼
Attacker retrieves file contents via job output / logs
Todos los pasos de esta PoC se ejecutaron y validaron en el siguiente sistema:
| Componente | Detalle |
|---|---|
| SO del anfitrión | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Arquitectura | x86_64 |
| Memoria total | 15.49 GiB |
| Docker Engine | 28.2.2 |
| JDK del anfitrión | OpenJDK 17.0.18 (usado solo por el anfitrión — 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 (compilación Scala 2.12) |
| Versión de Livy — imagen corregida | 0.9.0-incubating (compilación Scala 2.12) |