Що таке інʼєкція __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, недоторкано
__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. Одне твердження коштує майже нічого й ловить регресії від пізніших рефакторингів.