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, недоторкано

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

Додаткове читання