تخطي للذهاب إلى المحتوى

تلوّث النموذج الأولي في JavaScript: أمثلة وطرق الوقاية

كيف يصل __proto__ وconstructor.prototype إلى Object.prototype، وما يفعله المهاجمون بذلك في المتصفح وفي Node.js، وكيف تمنعه
4 يناير 2026 بواسطة
تلوّث النموذج الأولي في JavaScript: أمثلة وطرق الوقاية
Ibrahim Zakaria

تلوّث النموذج الأولي في JavaScript (Prototype Pollution) ثغرة يرسل فيها المهاجم بيانات تحمل مفتاحاً مثل __proto__ أو constructor.prototype، فينسخها الكود إلى Object.prototype، وهو الكائن الذي ترث منه كل الكائنات تقريباً. ومن تلك اللحظة يصبح لكل كائن في التطبيق الخاصية التي أضافها المهاجم. يشرح هذا المقال كيف يحدث ذلك بأمثلة قصيرة، وما الذي يؤدي إليه في المتصفح وفي Node.js، وكيف تمنعه.

سلسلة النماذج الأولية في دقيقتين

لكل كائن في JavaScript رابط خفي إلى كائن آخر هو نموذجه الأولي (Prototype). وحين تقرأ خاصية ما، تبحث JavaScript في الكائن نفسه أولاً، فإن لم تجدها تتبع الرابط إلى النموذج الأولي، ثم إلى النموذج الأولي لذلك الكائن، حتى تصل إلى Object.prototype في القمة.

const user = { name: "Rana" };
user.name;        // "Rana" (own property)
user.toString;    // found on Object.prototype
Object.getPrototypeOf(user) === Object.prototype;  // true
user.__proto__ === Object.prototype;               // true

من أي كائن عادي يوجد طريقان يعودان إلى Object.prototype. الأول هو الموصِّل __proto__، والثاني هو constructor.prototype، لأن user.constructor هو Object. وكل ما يُكتب في Object.prototype يظهر حين تقرأ تلك الخاصية من أي كائن لا يعرّفها بنفسه.

كيف يحدث تلوّث النموذج الأولي

يقع الخلل في الغالب في كود يمرّ على مفاتيح وصلته من الخارج ويكتبها في كائن. وتتكرر ثلاثة أنماط مرة بعد مرة: الدمج العميق أو النسخ العميق بالاستدعاء الذاتي، والدوال المساعدة التي «تضبط قيمة حسب المسار»، ومحلّلات سلاسل الاستعلام أو النماذج التي تحوّل a[b][c]=1 إلى كائنات متداخلة.

المثال 1: دمج عميق غير آمن

هذه دالة مساعدة من النوع الذي يجمع الإعدادات الافتراضية مع الإعدادات المرسلة في الطلب:

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object" && source[key] !== null) {
      if (typeof target[key] !== "object" || target[key] === null) {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// The request body, parsed from JSON
const body = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, body);

const someoneElse = {};
someoneElse.isAdmin;  // true

تحوّل JSON.parse النص "__proto__" إلى مفتاح عادي. وحين يصل إليه الدمج، تعيد target["__proto__"] الكائن Object.prototype، فتكتب المرحلة التالية من الدمج isAdmin فيه مباشرة. والآن تخيّل فحصاً للصلاحيات في موضع لاحق من الكود:

if (user.isAdmin) {
  // show the admin panel
}

كل كائن مستخدم لا يملك خاصية isAdmin خاصة به يجتاز هذا الفحص الآن، ولا يظهر أي خطأ ولا يُسجَّل شيء في السجلات.

المثال 2: الضبط حسب المسار عبر constructor.prototype

منع النص __proto__ وحده لا يكفي، فالدالة المساعدة التي تضبط قيمة من مسار مفصول بالنقاط يمكن الوصول إليها عبر constructor أيضاً:

function setByPath(obj, path, value) {
  const keys = path.split(".");
  let current = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    current[keys[i]] = current[keys[i]] || {};
    current = current[keys[i]];
  }
  current[keys[keys.length - 1]] = value;
}

setByPath({}, "constructor.prototype.isAdmin", true);
({}).isAdmin;  // true

{}.constructor هي الدالة Object، وObject.prototype هو النموذج الأولي المشترك، لذلك تقع الكتابة الأخيرة في المكان نفسه كما في المثال السابق.

أمثلة على تلوّث النموذج الأولي في مكتبات شائعة

شحنت حزم npm معروفة هذه الأنماط نفسها، ولهذا تحظى المشكلة باهتمام كبير:

  • lodash: كان من الممكن تلويث merge وmergeWith وdefaultsDeep في إصدارات 4.17.x الأقدم (CVE-2018-16487، CVE-2019-10744)، ثم حصلت zipObjectDeep لاحقاً على إصلاح خاص بها (CVE-2020-8203).
  • jQuery: كان $.extend(true, {}, input) قادراً على تلويث Object.prototype قبل الإصدار 3.4.0 (CVE-2019-11358).
  • minimist: كان محلّل وسائط سطر الأوامر هذا، الذي تستخدمه أدوات بناء كثيرة، يقبل وسائط من نوع --__proto__.x قبل الإصدار 1.2.6 (CVE-2020-7598، CVE-2021-44906).
  • Kibana: رُبطت ثغرة تلوّث نموذج أولي في ميزة Timelion بثغرات أخرى حتى وصلت إلى تنفيذ كود عن بُعد على الخادم (CVE-2019-7609).

وما زالت الإصدارات القديمة من هذه الحزم تظهر في ملفات القفل (Lock Files) وفي كود الواجهة المجمَّع وفي المقتطفات المنسوخة، بعد سنوات من صدور الإصلاحات.

ما الذي يستطيع المهاجم فعله بها

تلويث النموذج الأولي هو الخطوة الأولى فقط، ويتوقف الضرر على ما يقرؤه باقي الكود، ومنه المكتبات التي تستخدمها، من كائنات لا تضبط تلك الخاصية بنفسها. ويسمّي الباحثون الأمنيون هذا الكود «أداة» (Gadget).

في المتصفح

يأتي التلوّث في جهة المتصفح عادةً من الرابط، مثل ?__proto__[transport_url]=data:,alert(1)، أو من جزء بعد علامة # يُحلَّل إلى كائن. فإذا قرأ سكربت في الصفحة لاحقاً خياراً مثل رابط سكربت أو قالب HTML من كائن إعدادات لا يعرّفه، استُخدمت قيمة المهاجم. ويؤدي ذلك إلى البرمجة النصية عبر المواقع القائمة على DOM (DOM-based XSS)، التي قد تسرق الجلسات أو تتصرف باسم المستخدم المسجّل دخوله.

على الخادم (Node.js)

التلوّث على الخادم أخطر، لأن النموذج الأولي الملوَّث يبقى في عملية تعالج طلبات كل المستخدمين حتى يُعاد تشغيلها.

  • تصعيد الصلاحيات (Privilege Escalation): تصبح علامات الصلاحيات مثل isAdmin أو role صحيحة للجميع.
  • حجب الخدمة (Denial of Service): الكتابة فوق خاصية يعتمد عليها إطار العمل، مثل toString أو أحد خيارات الاستجابة، قد تعطّل كل الطلبات.
  • تنفيذ الكود عن بُعد (Remote Code Execution): بعض المكتبات تمرّر كائنات الخيارات إلى child_process أو إلى محرّكات القوالب، فيتحوّل خيار ملوَّث مثل shell أو متغيّر بيئة إلى أوامر تُنفَّذ على الخادم، كما أظهرت حالة Kibana.

كيف تمنع تلوّث النموذج الأولي

لا يوجد إجراء واحد يغطي كل شيء، لذلك اجمع بين عدد من الإجراءات التالية.

1. اكتب دوال دمج وضبط تتخطى المفاتيح الخطرة

انسخ المفاتيح الخاصة بالكائن وحدها، وارفض الأسماء الثلاثة التي تقود إلى نموذج أولي:

const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (BLOCKED.has(key)) continue;
    const value = source[key];
    if (value && typeof value === "object" && !Array.isArray(value)) {
      if (!Object.hasOwn(target, key) || typeof target[key] !== "object" || target[key] === null) {
        target[key] = {};
      }
      safeMerge(target[key], value);
    } else {
      target[key] = value;
    }
  }
  return target;
}

والأفضل من ذلك أن تستخدم قائمة سماح (Allow-list). فإذا كان كائن الإعدادات لا يحتوي إلا theme وlanguage وpageSize، انسخ هذه المفاتيح الثلاثة وتجاهل الباقي.

2. تحقّق من المدخلات وفق مخطط

تحقّق من كل محتوى طلب قبل استخدامه بمكتبة مخططات مثل Ajv (JSON Schema مع additionalProperties: false) أو Zod في الوضع الصارم، فتُرفض المفاتيح غير المعروفة، ومنها __proto__، قبل أن يراها الكود.

3. استخدم كائنات بلا نموذج أولي، أو Map

حين تأتي المفاتيح من المستخدمين، خزّنها في مكان لا يوجد فيه نموذج أولي يمكن الوصول إليه:

const lookup = Object.create(null);   // no Object.prototype behind it
lookup["__proto__"] = "just a key";

const sessions = new Map();           // keys never touch the prototype
sessions.set(userInput, data);

4. اعتمد على الخصائص الخاصة بالكائن في القرارات الأمنية

اقرأ علامات الصلاحيات بطريقة لا تكفي فيها القيم الموروثة، مثل Object.hasOwn(user, "isAdmin") && user.isAdmin === true، أو حمّل الأدوار من قاعدة البيانات في كل مرة بدل قراءتها من كائن مدموج.

5. اقفل النموذج الأولي

استدعاء Object.freeze(Object.prototype) عند بدء التطبيق يمنع أي كتابة لاحقة فيه. جرّبه أولاً، لأن بعض المكتبات القديمة تضيف دوالاً إلى النماذج الأولية المدمجة وستتعطل. وفي Node.js يزيل الخيار --disable-proto=delete أو --disable-proto=throw الموصِّل __proto__، لكنه لا يسدّ طريق constructor.prototype، لذلك اعتبره طبقة حماية إضافية.

6. حدّث الاعتماديات باستمرار

شغّل npm audit أو أداة فحص للاعتماديات ضمن عملية البناء، وحدّث lodash وjQuery وminimist وأي حزمة لسلاسل الاستعلام أو للدمج العميق تنبّهك إليها. وافحص ملف القفل أيضاً، لأن النسخة المعرّضة للثغرة تصل في كثير من الأحيان كاعتمادية لاعتمادية أخرى.

كيف تختبر تلوّث النموذج الأولي

  • اختبارات الوحدات (Unit Tests): مرّر إلى دوال الدمج والنسخ والضبط حسب المسار الحمولتين {"__proto__": {"polluted": 1}} و"constructor.prototype.polluted"، ثم تأكّد أن ({}).polluted ما زالت غير معرَّفة.
  • في المتصفح: افتح صفحة بعد إضافة ?__proto__[polluted]=1 أو #__proto__[polluted]=1 إلى رابطها، ثم اكتب Object.prototype.polluted في وحدة التحكم (Console). وتؤتمت أداة DOM Invader من PortSwigger، المدمجة في متصفح Burp Suite، هذه الخطوة وتبحث عن الأدوات (Gadgets).
  • على خادم Node.js تملكه: توثّق PortSwigger فحوصاً غير مدمّرة لتطبيقات Express، مثل إرسال خاصية json spaces داخل __proto__ ثم مراقبة تغيّر المسافات البادئة في ردود JSON اللاحقة. شغّل هذه الفحوص على بيئة اختبار، لأن الاختبار الناجح يلوّث العملية العاملة.
  • التحليل الثابت (Static Analysis): لدى Semgrep وCodeQL وSnyk Code قواعد للدمج بالاستدعاء الذاتي ولكتابة الخصائص الديناميكية، وهي المواضع التي تقع فيها معظم هذه الأخطاء.

لماذا يكرّر الكود المكتوب بالذكاء الاصطناعي هذا النمط

اطلب من مساعد ذكاء اصطناعي «دمجاً عميقاً» أو دالة «لضبط قيمة متداخلة حسب المسار» أو محلّلاً صغيراً لسلسلة الاستعلام، وستحصل في كثير من الأحيان على نسخة for...in البسيطة الظاهرة في المثال 1، لأنها النسخة التي رآها أكثر من غيرها، ونادراً ما يضيف فحصاً للمفاتيح إلا إذا طلبته. كما يقترح المساعد إصدارات حزم من بيانات تدريبه، وقد تكون أقدم من الإصدارات التي أصلحت الثغرة. والاختبارات التي يكتبها تمرّر بيانات سليمة، فلا يجرّب أحد __proto__ حتى يجرّبه مهاجم. فإذا كان جزء كبير من الكود مكتوباً بأدوات الذكاء الاصطناعي، فتلوّث النموذج الأولي من أول ما يستحق الفحص. ونتناول المشاكل الشائعة الأخرى في التطبيقات المبنية بالذكاء الاصطناعي، من المفاتيح المكشوفة إلى قواعد البيانات المفتوحة، في مقالنا عن المخاطر الأمنية للكود المكتوب بالذكاء الاصطناعي.

الأسئلة الشائعة

هل في JavaScript ثغرات أمنية؟

اللغة نفسها مستقرة، لكن طريقة عملها تجعل بعض الأخطاء سهلة الوقوع. فتلوّث النموذج الأولي ينشأ من الوراثة عبر النماذج الأولية في JavaScript، ويقف إلى جانب مشاكل شائعة أخرى في تطبيقات الويب، مثل البرمجة النصية عبر المواقع (XSS) والاستخدام غير الآمن لـeval وحزم npm القديمة التي لها ثغرات CVE معروفة.

ما هو Object.prototype في JavaScript؟

هو الكائن الموجود في قمة سلسلة النماذج الأولية. ترث منه الكائنات العادية دوالاً مثل toString وhasOwnProperty، لذلك تظهر أي خاصية تُضاف إلى Object.prototype في كل كائن عادي لا يعرّف تلك الخاصية بنفسه.

هل تلوّث النموذج الأولي مشكلة في Node.js وحده؟

لا، فهو يحدث في المتصفحات أيضاً، عادةً عبر معاملات الرابط التي تُحلَّل إلى كائنات، وقد يقود هناك إلى البرمجة النصية عبر المواقع. أما على خادم Node.js فالأثر أوسع، لأن عملية واحدة ملوَّثة تخدم كل المستخدمين، وقد يؤدي ذلك إلى تصعيد الصلاحيات أو توقف الخدمة، أو إلى تنفيذ كود عن بُعد مع بعض المكتبات.

هل يمنعه Object.freeze(Object.prototype) منعاً كاملاً؟

هو يمنع الكتابة في Object.prototype، وهذا يوقف الشكل الأكثر شيوعاً من الهجوم. لكن النماذج الأولية المدمجة الأخرى، مثل Array.prototype، تحتاج إلى التجميد أيضاً، وبعض المكتبات القديمة تتعطل حين تُجمَّد النماذج الأولية. لذلك اجمعه مع التحقق من المدخلات ودوال الدمج الآمنة.

هل ما زالت دالة merge في lodash معرّضة للثغرة؟

أُصلحت مشاكل تلوّث النموذج الأولي المعروفة في merge وmergeWith وdefaultsDeep في lodash 4.17.11 و4.17.12، وفي zipObjectDeep في 4.17.20. استخدم 4.17.21 أو أحدث، وافحص ملف القفل بحثاً عن نسخ متداخلة أقدم، وتحقّق من البيانات قبل دمجها في كل الأحوال.

كيف أعرف إن كان تطبيقي متأثراً؟

ابحث في الكود والاعتماديات عن دوال الدمج والنسخ والضبط حسب المسار بالاستدعاء الذاتي التي تتعامل مع بيانات المستخدمين، وشغّل npm audit، وجرّب الحمولات المذكورة أعلاه في بيئة اختبار. وفي قاعدة كود كبيرة أو مكتوبة بالذكاء الاصطناعي، تكون مراجعة الكود من جهة خارجية الطريق الأسرع.

افحص كود JavaScript الخاص بك

يسهل أن يفوت تلوّث النموذج الأولي مراجعةَ الكود، لأن الدالة المساعدة المعرّضة للثغرة تبدو غير مؤذية، والضرر يظهر في مكان آخر. وخدمة تقييم الكود المكتوب بالذكاء الاصطناعي لدينا تراجع أي تطبيق، أيّاً كان من كتبه أو الأداة التي كتبته، من حيث الأمان وأفضل الممارسات وسهولة الصيانة والجاهزية للتشغيل الفعلي. ويأتي كل اكتشاف مع خطوات إعادة إنتاجه وما يجب إصلاحه.

إن أردت أن تعرف أين يقف تطبيقك، فاحجز مكالمة مجانية مدتها 30 دقيقة وحدّثنا عن التقنيات التي تستخدمها.

تلوّث النموذج الأولي في JavaScript: أمثلة وطرق الوقاية
Ibrahim Zakaria 4 يناير 2026
شارك هذا المنشور
علامات التصنيف
راسلنا على واتسابوكيل ذكي يردّ على مدار الساعة
احجز مكالمة مجانية