Что такое инъекция __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. Одна проверка стоит почти ничего и ловит регрессии от последующих рефакторингов.