Prototype Pollution (CVE-2023-45811, CVE-2023-38894, CVE-2019-10744) — не новая брешь, вы уже наверняка читали про нее и на Хабре, и на PortSwigger, и даже в научных журналах, но есть нюанс. Несмотря на большое количество публикаций, некоторые популярные решения до сих пор остаются уязвимыми для нее. Очередной пациент — библиотека на TypeScript @clickbar/dot-diver. Уязвимость CVE-2023-45827 исправлена в версии 1.0.2 и выше, поэтому мы со спокойной душой расскажем, что могло произойти с вашим продуктом, но, к счастью, не произошло.

Под катом читайте о том, как нужно было пользоваться библиотекой, чтобы точно столкнуться с уязвимостью Prototype Pollution. Мы, кстати, писали про нее в своем телеграм-канале POSIdev — там свежие новости про безопасную разработку, AppSec, а также регулярные обзоры трендовых угроз и наша любимая рубрика «Пятничные мемы».

Итак, поехали!

Немного о Prototype Pollution

В JavaScript все сущности являются объектами, включая функции и определения классов. Процесс наследования реализован через модель прототипов. Каждый объект в JavaScript связан с особым объектом, который называется прототипом. Объект наследует все свойства связанного прототипа. Для доступа к прототипу объекта используется встроенное поле __proto__. Поиск любого поля в объекте производится по цепочке прототипов (prototype chain): сначала это поле ищется у объекта, потом у прототипа и далее — до самого верхнего уровня наследования.

Prototype Pollution позволяет атакующему «загрязнить» поле глобального объекта, которое может наследоваться пользовательскими объектами и создавать угрозу для безопасности приложения.

Основные условия успешной реализации атаки Prototype Pollution:

  1. Недоверенные входные данные, которые используются для «загрязнения» глобального объекта (prototype pollution source).

  2. Наличие уязвимых функций, которые могут приводить к проблемам безопасности в коде (sink).

  3. Возможность использования «загрязненного» поля глобального объекта без его фильтрации в уязвимой функции (exploitable gadget).

Механизм реализации атаки выглядит так:

  1. Атакующий «загрязняет» свойство глобального объекта через доступный объект.

  2. В приложении для уязвимой функции используется объект, который наследуется от «загрязненного» глобального объекта.

  3. Уязвимая функция использует поле из «загрязненного» глобального объекта, заданное атакующим, что приводит к нарушению безопасности.

Основные поля, используемые в атаках:

  • object.constructor.prototype.pullutedField

  • object.__proto__.pullutedField

Уязвимость в dot-diver

Библиотека @clickbar/dot-diver, написанная на TypeScript (source code), предоставляет удобный API для чтения значения поля из объекта (с помощью функции getByPath) и записи значения в поле объекта (с помощью функции setByPath).

Уязвимость была обнаружена в функции setByPath, которая принимает три аргумента:

  • object — объект, для свойства которого устанавливается значение,

  • path — шаблон пути до свойства, которое необходимо изменить,

  • value — значение, которое будет в поле по указанному пути.

При использовании функции setByPath происходит рекурсивный перебор полей в шаблоне пути. При нахождении необходимого поля ему присваивается нужное значение.

 Пример использования функции setByPath:

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

Код функции до исправления (версия 1.0.1):

Этот же код после исправления (версия 1.0.2):

 

В исправлении была добавлена проверка наличия собственного поля в объекте (с помощью функции Object.prototype.hasOwnProperty) перед его изменением. В случае отсутствия поля вызывается исключение с соответствующим сообщением.

Пример эксплуатации

В качестве примера используется приложение для учета прочитанных книг, в котором реализован механизм разграничения пользователей с разными ролями:

  • user — с правом внесения данных о прочитанных книгах и возможностью просмотра;

  • admin — с правом удаления книг из списка.

Разграничение доступа реализуется с помощью дополнительного поля isAdmin, которое есть только у объекта user с успешным результатом проверки учетных данных для роли admin. У других пользователей это поле отсутствует.

Фрагмент кода, выполняющий обработку запроса на удаление книг по ключу title и реализующий контроль прав доступа.

В приложении используются следующие команды API:

  • получить список книг:

$curl -v -X GET -H http://192.168.26.1:5000/

  • обновить список книг:

$curl -v -X PUT -H "Content-Type:application/json" --data '{"auth":{"name":"reader", "pwd":"books"},"title":"Tom Sawyer"}' http://192.168.26.1:5000/

  • удалить книгу с определенным именем:

$curl -v -X DELETE -H "Content-Type:application/json" --data '{"auth":{"name":"reader", "pwd":"books"},"title":"Tom Sawyer"}' http://192.168.26.1:5000/

После попытки удаления книги из списка от имени пользователя reader возвращается ответ с кодом 403 и текстом сообщения Access denied, сигнализирующий об отсутствии прав на выполнение таких действий.

В запрос добавляется полезная нагрузка для атаки:

$curl -v -X PUT -H "Content-Type:application/json" --data '{"auth":{"name":"reader", "pwd":"books"},"title":"Tom Sawyer", "note":"__proto__.isAdmin", "text":true}' http://192.168.26.1:5000/

В ответ на запрос отображается успешный результат обновления списка книг. Кроме того, у глобального объекта Object появилось поле isAdmin.

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

Меры противодействия

Основные рекомендации для устранения проблем безопасности, связанных с уязвимостью Prototype Pollution:

  • постоянное обновление пакетов до последних стабильных версий;

  • поиск уязвимых компонентов с помощью SCA и SAST;

  • обработка недоверенных пользовательских данных на входе приложения;

  • использование конструкторов в случае отсутствия необходимости в наследовании объектов:

    • let obj = Object.create(null),

    • let obj = {__proto__:null};

  • для защиты от модификации глобальных прототипов можно использовать функции API: Object.freeze() и Object.seal(). Но нужно учитывать, что они могут нарушить работу библиотек, в которых применяется механизм изменения атрибутов у глобальных прототипов.

Александр Болдырев

Ведущий специалист группы экспертизы статического анализа приложений

Дополнительные материалы

Карточка CVE

ID-CVE

CVE-2023-45827

CVSS 3.1

9,8

ID-CWE

CWE-1321

Type

Prototype Pollution

Impact

Зависит от последующего использования свойств объекта для реализации критически опасных функций (разграничения прав, исполнения кода, бизнес-логики)

Description package

Библиотека на TypeScript @clickbar/dot-diver до версии 1.0.1 включительно, которая предоставляет удобный API для чтения значения поля из объекта (функция getByPath) и записи значения в поле объекта (функция setByPath)

Комментарии (2)


  1. PaulIsh
    25.12.2023 13:06

    Без чтения исходного кода, где собственно этот setByPath и используется было не понятно как произошло загрязнение. Можно было подумать на bodyParser, но оказывается там есть метод `app.put('/', (req, res) ...` где собственно всё и происходит.


  1. ShADAMoV
    25.12.2023 13:06

    Большое спасибо за статью автору!

    Страшно представить, что может сделать человек знавший об этой уязвимости, который ушел обиженным из компании)) Одно дело, когда хакер, который особо не вникал в происходящее в коде, пытается нагадить, и совсем другое, когда нагадить хочет тот, кто этот код писал)