FlawOpen

flawopen.com/Prototype Pollution/Object.assign()

Защищает ли Object.assign() от Prototype Pollution?

CWE-1321: Prototype PollutionСправочная страница
Краткий ответ

Не полностью. Object.assign() копирует собственные перечисляемые свойства, присваивая их целевому объекту. Если у источника есть собственный перечисляемый ключ __proto__, присваивание может вызвать унаследованный setter и изменить прототип целевого объекта. Одно поверхностное присваивание само по себе не меняет общий Object.prototype; рекурсивное слияние может привести к более широкому воздействию.

НЕБЕЗОПАСНО — assign и рекурсивное слияние
// A JSON own "__proto__" key reaches the inherited setter
const source = JSON.parse('{"__proto__":{"isAdmin":true}}');
const target = Object.assign({}, source);
// target's prototype changes; Object.prototype does not.

// Recursive merge can reach Object.prototype
function deepAssign(target, source) {
  for (const key of Object.keys(source)) {
    if (source[key] && typeof source[key] === 'object') {
      target[key] = target[key] || {};
      deepAssign(target[key], source[key]);
      // With key === '__proto__', target[key] can be
      // Object.prototype; recursion then writes into it.
    } else {
      Object.assign(target, { [key]: source[key] });
    }
  }
}
БЕЗОПАСНО — spread для поверхностной копии; защищённое рекурсивное слияние
// Spread creates an own data property; it does not invoke the setter.
const shallow = { ...userInput };

// Reject dangerous keys at every depth and use own data properties.
const blocked = new Set(['__proto__', 'constructor', 'prototype']);
function isRecord(value) {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
  const proto = Object.getPrototypeOf(value);
  return proto === Object.prototype || proto === null;
}
function copySafe(value) {
  if (Array.isArray(value)) return value.map(copySafe);
  return isRecord(value) ? mergeSafe(value) : value;
}
function mergeSafe(source, target = Object.create(null)) {
  if (!isRecord(source)) throw new TypeError('Expected a plain object');
  for (const key of Object.keys(source)) {
    if (blocked.has(key)) continue;
    const value = source[key];
    const current = Object.hasOwn(target, key) ? target[key] : undefined;
    const next = isRecord(value)
      ? mergeSafe(value, isRecord(current) ? current : Object.create(null))
      : copySafe(value);
    Object.defineProperty(target, key, {
      value: next, enumerable: true, writable: true, configurable: true
    });
  }
  return target;
}

Что Object.assign() делает с ключом __proto__

Object.assign() копирует каждое собственное перечисляемое свойство источника обычным присваиванием в цель. Если источник из JSON имеет собственное свойство __proto__, а цель наследует устаревший setter, он меняет [[Prototype]] цели. Собственное буквальное свойство __proto__ в этом случае не создаётся.

Object spread ({ ...source }) создаёт свойства-данные в результате: такой ключ становится собственным свойством-данными и не вызывает унаследованный setter. Это полезное отличие для поверхностной копии, но не общая защита от последующего рекурсивного слияния.

Как рекурсивное слияние загрязняет общие прототипы

Prototype Pollution возникает, когда небезопасный рекурсивный код следует по ключам под контролем атакующего и записывает данные в общий прототип. Типичный шаблон читает target[key], чтобы найти вложенный объект. Если key равен __proto__, чтение может вернуть Object.prototype; следующая рекурсия тогда повлияет на обычные объекты глобально. Нужно учитывать и пути вроде constructor.prototype.

При проверке найдите чтение и запись в целевой объект по ключам атакующего, выясните, вызывают ли они setter, и убедитесь, что каждый уровень рекурсии отклоняет опасные ключи.

Вопросы и ответы

Такой же безопасный ли spread ({ ...obj })?

Да для поверхностной копии: spread создаёт собственное свойство-данные и не вызывает унаследованный setter. Object.assign(), напротив, может изменить прототип цели при перечисляемом ключе источника __proto__. Ни одна поверхностная операция сама по себе не меняет общий Object.prototype.

Что насчёт structuredClone()?

structuredClone() создаёт отдельную копию и не применяет цепочку прототипов источника. Собственный ключ-данные __proto__ остаётся данными в копии; последующее небезопасное слияние всё ещё может интерпретировать его особым образом.

Можно просто взять библиотеку?

Используйте поддерживаемую и обновлённую библиотеку слияния; проверьте обработку __proto__, constructor и prototype на каждом уровне. Для ряда популярных merge и object-path утилит публиковались предупреждения о Prototype Pollution; исправления есть только в обновлённых версиях.

Источники

См. также Загрязнение прототипа JavaScript: причины и защита Вызывает ли JSON.parse() загрязнение прототипа? JSON.parse() безопаснее, чем eval()? Защищён ли lodash.merge() от загрязнения прототипа?