Prototype Pollution

Prototype Pollution — атака на JavaScript-приложения, при которой злоумышленник модифицирует Object.prototype, добавляя свойства, которые наследуют все объекты в приложении, что может приводить к DoS, обходу проверок и RCE.

Зачем нужно

JavaScript использует прототипное наследование: каждый объект наследует свойства Object.prototype. Если атакующий может внедрить свойство в прототип через __proto__, constructor.prototype или Object.defineProperty, это затронет все объекты приложения.

Где используется

  • Функции глубокого копирования объектов (merge, clone, extend)
  • Парсинг пользовательских JSON-данных с __proto__ ключами
  • Template engines (Handlebars, Pug) — уязвимые версии
  • Библиотеки работы с путями объектов (lodash <4.17.12 — исторический пример)

Основной контент

Пример атаки

// Уязвимая функция deep merge
function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === 'object') {
      merge(target[key], source[key]); // Не проверяем key!
    } else {
      target[key] = source[key];
    }
  }
}

// Атакующий передаёт JSON:
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

// Теперь у ВСЕХ объектов есть isAdmin: true
const user = {};
console.log(user.isAdmin); // true — обход проверки прав!

Безопасный deep merge с проверкой ключей

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    // Запрещаем опасные ключи
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

Создание объекта без прототипа

// Object.create(null) создаёт объект без прототипа
const safeMap = Object.create(null);
safeMap['__proto__'] = 'harmless'; // Просто ключ, не прототип

// Использование Map вместо plain object
const map = new Map();
map.set('__proto__', 'harmless'); // Безопасно

JSON.parse + валидация

// JSON.parse не создаёт прototypechain, но Object.assign может перенести
const parsed = JSON.parse(input);

// Проверяем наличие опасных ключей
function hasProtoPollution(obj) {
  return '__proto__' in obj || 'constructor' in obj;
}

if (hasProtoPollution(parsed)) {
  throw new Error('Suspicious input');
}

Частые ошибки

  • Использование уязвимых версий lodash (.merge, .set, .defaultsDeep — до 4.17.12)
  • Рекурсивный merge объектов без проверки ключей __proto__ и constructor
  • Доверие JSON.parse без проверки структуры на опасные ключи
  • Использование hasOwnProperty как проверки в merge — не защищает от __proto__

Связанные темы

Ресурсы