מהי הזרקת __proto__
שורה תחתונה: JSON.parse מתייחס ל-__proto__ כמפתח רגיל; הנזק קורה ב-merge רקורסיבי שכותב ל-Object.prototype, כך שכל אובייקט יורש מאפיינים של התוקף.

שורה תחתונה: חסמו __proto__, constructor ו-prototype לפני כל merge, מזגו קלט לא מהימן לתוך Object.create(null), והוסיפו assertion של prototype ל-CI; עדכון ספריות לבדו לא מספיק.
במשפט אחד: מפתח לא מהימן בשם __proto__ פוגש merge רקורסיבי, וה-merge כותב לפרוטוטייפ.
שחזור
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 מגיע לאותו מקום.
מזגו לאובייקט עם פרוטוטייפ null.
const safeTarget = Object.create(null);
בלי שרשרת פרוטוטייפ, __proto__ הוא רק מפתח מחרוזת.
השתמשו ב-Object.hasOwn במקום in. in מתייעץ עם שרשרת הפרוטוטייפ.
שמרו על תלויות מעודכנות. lodash, deepmerge ו-json-patch הוציאו תיקונים לכל סוג הבאג הזה. עקבו אחרי npm audit.
ודאו שזה נשאר מתוקן
העבירו payload שמכיל __proto__ ו-constructor דרך ה-merge שלכם, ואז בדקו:
console.log({}.polluted); // undefined
console.log(Object.prototype.polluted); // undefined
Assertion אחד ב-CI, עלות כמעט אפסית, תופס רגרסיות מרפקטורים מאוחרים יותר.
שאלות ותשובות
אילו מפתחות merge צריך לסנן?
חסמו __proto__, constructor ו-prototype יחד. סינון של __proto__ לבדו לא מספיק, כי constructor.prototype מגיע לאותו פרוטוטייפ.
למה Object.create(null) עוצר את הכתיבה?
לאובייקט עם פרוטוטייפ null אין שרשרת פרוטוטייפ, כך ש-__proto__ נשאר מפתח מחרוזת רגיל. הוא עובד כמכל נתונים, אבל הוא לא מחליף סינון מפתחות או עדכון תלויות.
איך בודקים prototype pollution ב-CI?
שלחו payload שמכיל __proto__ ו-constructor דרך ה-merge שלכם, ואז בדקו ש-{}.polluted ו-Object.prototype.polluted הם undefined. assertion אחד עולה כמעט כלום ותופס רגרסיות מרפקטורים מאוחרים יותר.