
Una POC per la vulnerabilità di bypass della whitelist di Path Traversal in Apache Livy
Solo per scopi educativi e di ricerca sulla sicurezza. Non utilizzare contro sistemi di cui non sei proprietario o per i quali non hai esplicita autorizzazione scritta a effettuare test. → Dichiarazione di non responsabilità completa
| Campo | Dettaglio |
|---|---|
| ID CVE | CVE-2025-66249 |
| Gravità | Importante (CVSS non disponibile — valutazione NVD in sospeso al 2026-03-15) |
| Versioni interessate | Apache Livy 0.3.0-incubating fino a 0.8.0-incubating — solo quando livy.file.local-dir-whitelist è impostato su un valore non predefinito |
| Corretto in | Apache Livy 0.9.0-incubating |
| CWE | CWE-22: Limitazione impropria di un percorso a una directory ristretta ('Path Traversal') |
| Divulgata | 2026-03-12 (OSS-Sec) / 2026-03-13 (NVD) |
| Segnalatore | Hiroki Egawa (ricercatore) |
Un utente autenticato con accesso all'interfaccia REST o JDBC di Livy può inviare una sessione Spark o un job batch con un valore di configurazione del percorso file manipolato che fuoriesce dalla whitelist di directory consentite.
Causa principale — Bypass del controllo della whitelist tramite path traversal (Session.scala)
Quando livy.file.local-dir-whitelist è configurato, Livy 0.8.0 valida i percorsi
inviati chiamando String.startsWith() di Java sul percorso grezzo, non normalizzato.
Questo controllo può essere bypassato usando sequenze di traversal ../:
/opt/safe-data/../sensitive/secret.txt
La stringa grezza inizia con /opt/safe-data, quindi il controllo viene superato —
ma il percorso risolve in /opt/sensitive/secret.txt, che è completamente fuori
dalla directory in whitelist.
Condizione di attivazione: La vulnerabilità può essere sfruttata solo quando
livy.file.local-dir-whitelist è impostato su un valore non predefinito (non vuoto).
Se la whitelist è vuota (impostazione predefinita), la validazione del percorso viene
completamente saltata e il problema non si manifesta.
Impatto: Un attaccante che invia una sessione tramite l'API REST di Livy può fare riferimento a file locali arbitrari sull'host del server Livy. In un cluster di analisi condiviso ciò si traduce in una potenziale esposizione di credenziali, chiavi, file di configurazione o qualsiasi dato leggibile dall'utente del processo Livy.
Session.scalaVulnerabile (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Corretto (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Entrambe le versioni sono state clonate direttamente dal repository GitHub ufficiale di Apache Livy usando i seguenti comandi esatti:
Repository: https://github.com/apache/incubator-livy
# Vulnerable version — cloned into ./livy-0.8.0/
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# Fixed version — cloned into ./livy-0.9.0/
git clone --depth=1 --branch v0.9.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.9.0
| Versione | Tag | Commit risolto | Percorso locale |
|---|---|---|---|
| 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() prima del controllo della whitelist 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.")
}
Impatto in v0.8.0:
Il controllo startsWith sulla stringa grezza può essere bypassato con un payload di path traversal.
Esempio: se 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 (bypassato)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → false (bloccato)I diff sono stati prodotti clonando entrambi i tag localmente (vedi sopra) ed eseguendo:
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
Attacker (authenticated REST/JDBC user)
│
▼
POST /sessions
{
"conf": {
"spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
← path starts with whitelisted prefix — String.startsWith() passes
← but resolves OUTSIDE the directory via ../ traversal
}
}
│
▼
Livy 0.8.0 — whitelist check bypassed (raw startsWith, no normalisation)
│
▼
Spark reads the file and distributes it to executors
│
▼
Attacker retrieves file contents via job output / logs
Tutti i passaggi di questo PoC sono stati eseguiti e validati sul seguente sistema:
| Componente | Dettaglio |
|---|---|
| Sistema operativo host | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Architettura | x86_64 |
| Memoria totale | 15 GiB |
| Docker Engine | 28.2.2 |
| JDK host | OpenJDK 17.0.18 (usato solo dall'host — i container usano eclipse-temurin:11-jdk-focal) |
| Immagine base del container | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| Versione Spark (entrambe le immagini) | 3.1.3 con Hadoop 3.2 |
| Versione Livy — immagine vulnerabile | 0.8.0-incubating |
| Versione Livy — immagine corretta | 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