
Una POC per la vulnerabilità di accesso non autorizzato ai file di Apache Livy
Solo per scopi educativi e di ricerca sulla sicurezza. Non utilizzare contro sistemi che non possiedi o per i quali non hai esplicita autorizzazione scritta al test. → Disclaimer completo
| Field | Detail |
|---|---|
| CVE ID | CVE-2025-60012 |
| Gravità | Media (CVSS 6.3) |
| Versioni interessate | Apache Livy 0.7.0-incubating, 0.8.0-incubating — quando connesso ad Apache Spark 3.1 o versioni successive |
| Corretta in | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: Improper Input Validation |
| Divulgata | 2026-03-13 |
| Segnalato da | Furue Hideyuki |
Un utente autenticato con accesso all'interfaccia REST o JDBC di Livy può inviare una sessione Spark o un job batch con valori di configurazione predisposti ad arte. Due debolezze combinate consentono all'attaccante di referenziare file del filesystem locale al di fuori dei percorsi consentiti:
Validazione mancante per spark.archives — Spark 3.1 ha introdotto spark.archives come
metodo unificato per distribuire file di archivio su tutti i cluster manager. La lista hardcoded delle chiavi di configurazione di Livy 0.8.0
sottoposte a validazione dei percorsi (HARDCODED_SPARK_FILE_LISTS) non
include spark.archives. Un percorso passato tramite questa chiave non viene quindi mai controllato contro la
whitelist del filesystem locale (livy.file.local-dir-whitelist), consentendo a un attaccante di
referenziare qualsiasi file locale.
Bypass del path traversal nel controllo della whitelist — Anche per le chiavi di configurazione che vengono
validate, il confronto con la whitelist in Livy 0.8.0 usa una semplice chiamata Java String startsWith
sul percorso grezzo. Un attaccante può aggirarlo usando path traversal:
/whitelisted/dir/../../etc/passwd supera il controllo sulla stringa ma risolve al di fuori della
directory consentita.
LivyConf.scalaVulnerabile (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Corretto (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
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 in questo workspace utilizzando i seguenti comandi esatti:
Repository: 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
| 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/` |
## Diff esatti del codice
I diff sono stati prodotti clonando entrambi i tag in locale (vedi sopra) ed eseguendo:```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 aggiunto all'elenco di file hardcoded```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**Impatto della voce mancante in v0.8.0:**
Quando un utente invia una sessione con `conf: {"spark.archives": "file:///etc/passwd"}`, Livy
0.8.0 non chiama mai `resolveURIs()` su quel valore e non lo controlla mai rispetto a
`livy.file.local-dir-whitelist`. Il percorso viene inoltrato a Spark senza convalida.
---
### Correzione 2 — `Session.scala`: normalizzazione del percorso prima del controllo della whitelist```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.")
}
}
Impatto in v0.8.0:
Il controllo startsWith sulla stringa grezza può essere aggirato con un payload di path traversal.
Esempio: se `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** (bypassato)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (bloccato)
## Riepilogo dei Vettori di Attacco```
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
Tutti i passaggi di questa 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.49 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 (build Scala 2.12) |
| Versione Livy — immagine corretta | 0.9.0-incubating (build Scala 2.12) |