JSON Toolkit

Что такое инъекция __proto__

Главное: JSON.parse трактует __proto__ как обычный ключ; ущерб происходит в рекурсивном слиянии, которое пишет в Object.prototype, поэтому каждый объект наследует свойства атакующего.

Продвинутый~5 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Как работает загрязнение прототипа и как его заблокировать

Главное: блокируйте __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. Одна проверка стоит почти ничего и ловит регрессии от последующих рефакторингов.

Дополнительное чтение