ما هو حقن __proto__
الخلاصة: JSON.parse يعامل __proto__ كمفتاح عادي؛ والضرر يحدث في دمج تكراري يكتب إلى Object.prototype، فيرث كل كائن خصائص المهاجم.

الخلاصة: احجب __proto__ وconstructor وprototype قبل أي دمج، وادمج المدخلات غير الموثوقة في Object.create(null)، وأضف تأكيدًا للنموذج الأولي في CI؛ إصلاح المكتبات وحده لا يكفي.
بجملة واحدة: مفتاح غير موثوق اسمه __proto__ يقابل دمجًا تكراريًا، فيكتب الدمج إلى النموذج الأولي.
إعادة إنتاج
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log({}.isAdmin); // true
لم يُمس كائن أعمال واحد. لكن Object.prototype مُس، وكل كائن الآن يحمل isAdmin.
لماذا لا ذنب لـJSON.parse
const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data); // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype; // true, untouched
يصل __proto__ كخاصية مملوكة عادية. والخطر كله في الكود الذي يدمج لاحقًا أو ينسخ نسخًا عميقًا أو يعيّن بالمسار — _.merge، وبعض أنماط Object.assign، وعدد لا يحصى من مساعدات set(obj, path, value) المكتوبة يدويًا.
الأثر
- تجاوز التصريح: يصير
if (user.isAdmin)صحيحًا بلا شرط - فساد البيانات: حقول غير متوقعة تُسلسل إلى سجلات مخزنة
- حرمان الخدمة: كائنات كبيرة ملحقة بالنموذج الأولي تزيد الذاكرة دائمًا
- التصعيد: بالاشتراك مع محركات القوالب أو مشغلات الأوامر، أدى هذا إلى تنفيذ كود عن بعد
أربع دفاعات
رشّح المفاتيح.
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
حجب __proto__ وحده غير كافٍ: constructor.prototype يصل إلى المكان نفسه.
ادمج في كائن بلا نموذج أولي.
const safeTarget = Object.create(null);
بلا سلسلة نموذج أولي، يبقى __proto__ مجرد مفتاح سلسلة.
استخدم Object.hasOwn بدلًا من in. in يستشير سلسلة النموذج الأولي.
أبقِ التبعيات مصلحة. lodash وdeepmerge وjson-patch أصدرت جميعها إصلاحات لهذه الفئة من الثغرات. راقب npm audit.
تحقق من بقائه مصلحًا
مرّر حمولة تحتوي على __proto__ وconstructor عبر دمجك، ثم أكّد:
console.log({}.polluted); // undefined
console.log(Object.prototype.polluted); // undefined
تأكيد واحد في CI، بكلفة تقارب الصفر، يلتقط الانحدارات من إعادات الهيكلة اللاحقة.
الأسئلة الشائعة
ما المفاتيح التي يجب أن يرشحها الدمج؟
احجب __proto__ وconstructor وprototype معًا. ترشيح __proto__ وحده غير كافٍ، لأن constructor.prototype يصل إلى النموذج الأولي نفسه.
لماذا يوقف Object.create(null) الكتابة؟
كائن بلا نموذج أولي لا يملك سلسلة نموذج أولي، فيبقى __proto__ مفتاح سلسلة عاديًا. يعمل حاوية بيانات، لكنه لا يغني عن ترشيح المفاتيح أو تحديث التبعيات.
كيف أختبر تلوث النموذج الأولي في CI؟
أرسل حمولة تحتوي على __proto__ وconstructor عبر دمجك، ثم أكّد أن {}.polluted وObject.prototype.polluted قيمتهما undefined. التأكيد الواحد لا يكلف شيئًا تقريبًا ويلتقط الانحدارات من إعادات الهيكلة اللاحقة.