Prototype Pollution: загрязнение Object.prototype через recursive merge
Эндпоинт обновления пользовательских настроек в Express делает recursive merge тела запроса без фильтрации ключа __proto__. Загрязни Object.prototype.isAdmin и получи доступ к /admin/flag.
mediumnodejsPro
Задача
# Prototype Pollution через recursive merge
Интернет-магазин **SecureShop** позволяет каждому покупателю хранить персональные настройки профиля: тему оформления, параметры уведомлений по email/SMS, локаль. На странице `/profile` есть карточка Preferences, которая делает `POST /profile/preferences` с JSON-телом — сервер сливает входящий документ с текущими настройками пользователя. Слияние выполняется внутренней функцией с поддержкой вложенных объектов любой глубины, чтобы можно было апдейтить только часть настроек, не пересылая весь объект целиком.
В админской части магазина есть служебный эндпоинт `GET /admin/flag`. Он возвращает CTF-флаг, если у текущего пользователя установлен признак администратора. Никто из существующих учётных записей таким признаком в памяти процесса не обладает: ни `demo`, ни `admin` не значатся как `isAdmin: true` (поле `role` в таблице — только для отображения в UI, проверка авторизации читает другое свойство). Попытка обратиться к `/admin/flag` под обычным аккаунтом возвращает `403`.
## Что мы хотим продемонстрировать
JavaScript-объекты наследуют свойства через цепочку прототипов. У каждого «обычного» объекта в Node есть невидимая ссылка на общий объект-родитель, и все имена, которые не определены у самого объекта, ищутся в этом родителе. Если приложение позволяет атакующему задавать произвольные ключи во вложенном объекте без фильтрации служебных имён (тех, что ведут к прототипу), один JSON-документ может изменить поведение всех существующих и будущих объектов в процессе.
Этот класс уязвимости называется **Prototype Pollution**. Опасность не в порче конкретного объекта, а в глобальном эффекте: после успешной атаки любая последующая проверка вида `if (obj.someFlag)` для произвольного объекта вернёт «правильное» с точки зрения атакующего значение, даже если самому объекту этот флаг никогда не назначался.
Цель — авторизуйся в системе как обычный пользователь `demo`, через эндпоинт обновления настроек подмени значение, которое читает админская проверка, на уровне общего прототипа, после чего обратись к `/admin/flag` и забери награду.
## Точки входа
- Форма `POST /login` — обычная авторизация. Тестовая учётная запись `demo / demo`.
- Эндпоинт `POST /profile/preferences` — принимает JSON, рекурсивно мержит в объект настроек пользователя. Сюда поступают пользовательские данные без валидации формы. На странице `/profile` есть готовая форма для отправки.
- Эндпоинт `GET /admin/flag` — выдаёт флаг при наличии у пользователя признака администратора. Запрос обязательно с куками сессии.
## Почему это важно
Prototype Pollution особенно опасна тем, что не оставляет следов в БД: записанные данные остаются «обычными» настройками пользователя, потому что сериализация в JSON естественным образом игнорирует унаследованные поля. В памяти процесса при этом меняется поведение целого класса объектов. Любой код, который читает поле через цепочку прототипов (`if (user.isAdmin)` без явной проверки на собственное свойство), теперь возвращает «правильный» с точки зрения атакующего ответ.
Эффект сохраняется до перезапуска процесса. В реальных сервисах одной успешной атаки достаточно, чтобы превратить весь pool worker'ов в скомпрометированный — и это касается не только админских флагов, но и любой логики, опирающейся на наличие/отсутствие поля.