
CVE-2026-102282: تصعيد صلاحيات محلي (LPE) في adm-zip عبر الحفاظ على SUID/SGID أثناء استخراج الأرشيف (تم إصلاحه في 0.6.1)
إسناد أذونات غير صحيح في حزمة npm adm-zip. يمكن للاستخراج أن يحتفظ ببتات setuid وsetgid وsticky من ملف zip غير موثوق عندما يكون keepOriginalPermission مُفعَّلاً، مما يسمح بزرع ملف تنفيذي setuid مملوك لـ root على القرص من أرشيف يوفره المهاجم: تصعيد صلاحيات محلي.
| الحزمة | adm-zip (npm) |
| النظام البيئي | Node.js |
| CVE | CVE-2026-102282 |
| التنبيه | GHSA-j5f4-cc29-5x44 |
| CWE | CWE-732 إسناد أذونات غير صحيح لمورد حرج |
| CVSS v3.1 | 7.1 High — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| المتأثرة | < 0.6.1 (آخر إصدار متأثر معروف: <= 0.6.0) |
| المُصلَحة | 0.6.1 (11 سبتمبر 2026) |
| تاريخ النشر | تنبيه المشرف 11 سبتمبر 2026؛ قاعدة بيانات تنبيهات GitHub 29 سبتمبر 2026 |
يحتوي هذا المستودع على التقرير، وأداة للتحقق من إصدار التبعية، واختبار سلوكي آمن، وإثبات مفهوم مكتفٍ ذاتيًا مبني حول ملف تحقق حميد لا يفعل سوى طباعة سياق بيانات اعتماده.
كل إصدار قبل 0.6.1 متأثر، بما في ذلك 0.6.0.
| النطاق | الحالة |
|---|---|
>= 0 و < 0.6.1 | متأثر |
<= 0.6.0 | آخر إصدار متأثر معروف |
>= 0.6.1 | مُصلَح |
لا يمكن الوصول إلى الخلل إلا عندما تكون جميع الشروط التالية صحيحة:
keepOriginalPermission=true إلى extractAllTo() أو extractAllToAsync() أو extractEntryTo().الاستخراج الافتراضي يترك العَلَم مُعطَّلاً وغير متأثر. الاستخراج كمستخدم غير ذي امتيازات يمكنه مع ذلك تطبيق بتات الوضع، لكن الملف الناتج يملكه نفس المستخدم، فلا يعبر حدود الامتيازات.
توجد بتات وضع Unix في الـ 16 بت العلوية من سمات الملف الخارجية لمدخل zip. ينسخ adm-zip تلك البتات إلى المسار المستخرج.
| الموقع | الدور |
|---|---|
headers/entryHeader.js — fileAttr | يقرأ وضع Unix من السمات الخارجية |
adm-zip.js — extractAllTo، extractAllToAsync، extractEntryTo | يستخدم fileAttr عندما يكون keepOriginalPermission صحيحًا |
util/utils.js — chmodSync داخل مساعد كتابة الملف | يطبّق ذلك الوضع على المسار المُنشأ للتو |
استخراج المجلدات في adm-zip.js | يطبّق نفس الوضع على المجلدات المستخرجة |
في 0.6.0 وما قبلها، يقوم fileAttr بإخفاء السمة المُزاحة باستخدام 0xfff
(0o7777). يحتفظ هذا القناع بالبتات الخاصة الثلاث إلى جانب بتات rwx
المعتادة، ولا شيء في مسار الكتابة يمسح 0o7000 قبل chmod:
// headers/entryHeader.js (<= 0.6.0)
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
0o4000)0o2000)0o1000)يمرر الاستخراج الوضع الذي يتحكم به الأرشيف مباشرة إلى نظام الملفات:
// adm-zip.js
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeFileTo(entryName, content, overwrite, fileAttr);
// util/utils.js
self.fs.chmodSync(path, attr || 0o666);
لاحظ توقيع الاستدعاء في 0.6.x: extractAllTo(targetPath, overwrite, keepOriginalPermission) يأخذ قيمة منطقية حرفية كوسيطه الثالث.
تمرير كائن خيارات مثل { keepOriginalPermission: true } يُحوَّل بصمت إلى false، مما يحجب الثغرة في اختبار بدلاً من إظهارها. كلا سكربتات PoC في هذا المستودع تمرر true مباشرة.
الإصلاح هو الالتزام 6a63c339 (v0.6.1). يحتفظ fileAttr الآن ببتات rwx التسع فقط:
get fileAttr() {
return (_attr || 0) >> 16 & 0o777;
}
keepOriginalPermission=true، يقرأ الاستخراج ذلك الوضع عبر fileAttr ويمرره إلى chmod.0o7777، تنجو بتات setuid وsetgid وsticky.chmod بصلاحيات root، يصبح الملف الجديد مملوكًا لـ root ويمكنه حمل setuid. تشغيل لاحق من مستخدم أقل امتيازًا ينفّذ كود مالك الملف بصلاحيات root.مع ترك العَلَم على وضعه الافتراضي، لا يطبّق الاستخراج وضع الأرشيف، ولا تُكتب البتات الخاصة.
كل شيء موجود تحت poc/. الحمولة المرفقة هي
verify.c، وهو برنامج حميد يطبع uid/gid الحقيقي والفعال، وفي وضعه الافتراضي، يفتح صدفة بالهوية الفعالة. لا يستمر، ولا يخفي، ولا يفعل أي شيء يتجاوز إظهار سياق بيانات الاعتماد.
| الملف | الغرض |
|---|---|
poc/verify.c | حمولة تحقق setuid حميدة |
poc/build-archive.js | يحزم ملفًا في zip يحمل مدخله الوضع 0o4755 |
poc/extract.js | يستخرج أرشيفًا باستخدام keepOriginalPermission=true، كما يفعل تطبيق متأثر |
poc/run-demo.sh | تشغيل شامل: بناء، استخراج بصلاحيات root، تشغيل كمستخدم المستدعي |
git clone <this repo> && cd CVE-2026-102282
npm install [email protected] # the version under test
sudo apt install gcc # if needed
./poc/run-demo.sh # uses /var/tmp/cve-2026-102282-demo
./poc/run-demo.sh /path/to/out # or a custom output directory
المخرجات المتوقعة مقابل 0.6.0:
== 3. extract as root with keepOriginalPermission ==
adm-zip version: 0.6.0
extracted /var/tmp/cve-2026-102282-demo/exploit.zip into /var/tmp/cve-2026-102282-demo/extracted
== 4. result on disk ==
4755 -rwsr-xr-x root:root
== 5. run as the unprivileged user (user) ==
uid=1000(user) ...
[*] real uid=1000 gid=1000
[*] eff. uid=0 gid=0
[+] SUID active: effective uid 0 differs from real uid 1000
المخرجات المتوقعة مقابل 0.6.1:
== 4. result on disk ==
755 -rwxr-xr-x root:root
== 5. run as the unprivileged user (user) ==
uid=1000(user) ...
[*] real uid=1000 gid=1000
[*] eff. uid=1000 gid=1000
[-] effective and real ids match: no SUID context
تحذيرات للعرض التجريبي:
nosuid؛ يحذّر السكربت عند اكتشاف خيار التثبيت هذا.sudo من حسابك الخاص حتى يكون لدى الخطوة 5 حساب غير root للانتقال إليه).يتحقق scripts/behavior-test.js من نسخة adm-zip
المثبتة مباشرة، دون root ودون حمولة: يبني مدخل zip في الذاكرة يحمل 0o4755،
ويستخرجه باستخدام keepOriginalPermission=true، ويتحقق مما إذا كان بت setuid قد نجا.
npm install [email protected] && node scripts/behavior-test.js # -> exit 1, VULNERABLE
npm install [email protected] && node scripts/behavior-test.js # -> exit 0, not vulnerable
تصعيد صلاحيات محلي للتطبيقات وخطوط الأنابيب التي تستخرج ملفات zip غير موثوقة بصلاحيات root مع keepOriginalPermission=true.