Prototype pollution

Механизм прототипного загрязнения в JavaScript

Прототипное загрязнение (prototype pollution) — класс уязвимостей, возникающих при неконтролируемом изменении цепочки прототипов объектов в JavaScript. Поскольку большинство объектов наследуются от Object.prototype, добавление или модификация свойств на уровне прототипа приводит к их появлению во всех объектах приложения.

Типичный вектор атаки связан с присвоением значений по ключам:

  • __proto__
  • constructor.prototype
  • prototype

При некорректной обработке входных данных возможно выполнение кода вида:

obj["__proto__"]["polluted"] = "value";

Результат — появление свойства polluted во всех объектах, даже тех, которые не имели к нему отношения.

Особенность уязвимости заключается в том, что она часто проявляется не напрямую, а через цепочки преобразований данных: merge-операции, глубокое копирование, десериализацию JSON и автоматическое построение объектов.


Joi как инструмент валидации объектов

Joi представляет собой библиотеку для описания и проверки структур данных. Основная задача — гарантировать соответствие входных объектов заданной схеме.

Типовой сценарий использования:

const schema = Joi.object({
  username: Joi.string().alphanum().required(),
  age: Joi.number().min(0).max(120)
});

const result = schema.validate(input);

Joi работает на уровне описания структуры, а не на уровне прямого манипулирования прототипами. Однако взаимодействие с объектами JavaScript в процессе валидации и трансформации данных создаёт потенциальные точки риска при неправильной интеграции.


Потенциальные точки риска при использовании Joi

Обработка вложенных объектов

При валидации сложных структур Joi допускает глубокие схемы:

const schema = Joi.object({
  settings: Joi.object({
    theme: Joi.string(),
    config: Joi.object()
  })
});

Если результат валидации напрямую используется для сборки новых объектов без фильтрации ключей, появляется риск переноса опасных свойств.


Ключи объектов и динамическая структура

JavaScript допускает любые строковые ключи:

const input = JSON.parse('{"__proto__": {"admin": true}}');

Если валидированный объект используется в дальнейшем без фильтрации ключей, например при merge-операциях:

Object.assign(target, validatedData);

или:

lodash.merge(target, validatedData);

возможно распространение нежелательных свойств на прототип.


Поведение при разрешении дополнительных полей

Joi поддерживает режимы:

  • unknown(true) — разрешает неизвестные поля
  • unknown(false) — запрещает лишние поля

Режим unknown(true) в сочетании с отсутствием фильтрации ключей после валидации может привести к сохранению потенциально опасных свойств:

const schema = Joi.object({
  name: Joi.string()
}).unknown(true);

При таком подходе любые дополнительные поля сохраняются в результате.


Как Joi влияет на поверхность атаки

Сама библиотека Joi не изменяет прототипы объектов и не выполняет операций, которые напрямую ведут к prototype pollution. Однако она участвует в следующих процессах:

  1. Формирование результирующих объектов после валидации
  2. Трансформация входных данных (casting, default values)
  3. Разрешение вложенных структур
  4. Пропуск неизвестных ключей при определённых настройках

Именно на этапе интеграции с бизнес-логикой возникает риск.


Типичные анти-паттерны использования

Прямое использование результата валидации как конфигурации

const config = schema.validate(userInput).value;
globalConfig = Object.assign(globalConfig, config);

Если config содержит специальные ключи, они могут попасть в глобальные структуры.


Глубокое слияние без фильтрации

merge(appState, validatedInput);

Без очистки ключей __proto__ и constructor возможно загрязнение цепочки прототипов.


Использование входных данных для построения объектов

const obj = {};
for (const key in validatedInput) {
  obj[key] = validatedInput[key];
}

Цикл for...in может обходить свойства, унаследованные через прототип, если объект уже был загрязнён.


Защитные механизмы на уровне схем Joi

Ограничение структуры объекта

Joi.object({
  name: Joi.string()
}).required().unknown(false);

Запрет дополнительных ключей снижает вероятность передачи неожиданных свойств.


Явная валидация ключей

Joi позволяет задавать строгие схемы для ключей:

Joi.object().pattern(
  /^[a-zA-Z0-9]+$/,
  Joi.string()
);

Это исключает использование специальных ключей вроде __proto__.


Нормализация данных после валидации

После выполнения валидации целесообразно применять очистку:

  • удаление __proto__
  • удаление constructor
  • удаление prototype
delete sanitized.__proto__;
delete sanitized.constructor;

Безопасные стратегии работы с объектами

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

const safeObject = Object.create(null);

Такой объект не наследует свойства Object.prototype, что снижает эффект загрязнения.


Использование безопасных операций merge

Некоторые реализации merge позволяют игнорировать опасные ключи:

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === "__proto__" || key === "constructor") continue;
    target[key] = source[key];
  }
}

Изоляция данных после валидации

Результаты Joi следует рассматривать как данные, требующие дополнительного уровня защиты перед использованием в:

  • конфигурационных объектах
  • глобальных состояниях
  • кэше
  • системах плагинов

Взаимодействие Joi с десериализацией JSON

В связке:

JSON.parse → Joi.validate → merge

возможна цепочка, в которой опасные ключи проходят через этапы обработки, если отсутствует фильтрация.

Особенно критично это при обработке внешних API или пользовательского ввода.


Особенности поведения вложенных схем

При глубокой валидации:

Joi.object({
  a: Joi.object({
    b: Joi.object({
      c: Joi.string()
    })
  })
});

Joi рекурсивно проходит по структуре, но не интерпретирует ключи как опасные сами по себе. Опасность возникает только при последующем использовании результата без очистки.


Роль контекста выполнения

Prototype pollution становится значимой проблемой в следующих окружениях:

  • серверные приложения Node.js
  • SSR (server-side rendering)
  • системы плагинов
  • multi-tenant архитектуры

В таких системах загрязнение прототипа может влиять на множество независимых компонентов одновременно.


Принципы безопасной интеграции Joi

  • строгая схема без неизвестных ключей
  • отсутствие прямого merge входных данных в глобальные объекты
  • использование объектов без прототипа для хранения конфигураций
  • фильтрация ключей на уровне бизнес-логики
  • запрет передачи служебных ключей через границы слоёв приложения
  • контроль всех операций глубокого объединения объектов