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:
Недоверенные входные данные, которые используются для «загрязнения» глобального объекта (prototype pollution source).
Наличие уязвимых функций, которые могут приводить к проблемам безопасности в коде (sink).
Возможность использования «загрязненного» поля глобального объекта без его фильтрации в уязвимой функции (exploitable gadget).
Механизм реализации атаки выглядит так:
Атакующий «загрязняет» свойство глобального объекта через доступный объект.
В приложении для уязвимой функции используется объект, который наследуется от «загрязненного» глобального объекта.
Уязвимая функция использует поле из «загрязненного» глобального объекта, заданное атакующим, что приводит к нарушению безопасности.
Основные поля, используемые в атаках:
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 для чтения значения поля из объекта (функция |
Комментарии (2)
ShADAMoV
25.12.2023 13:06Большое спасибо за статью автору!
Страшно представить, что может сделать человек знавший об этой уязвимости, который ушел обиженным из компании)) Одно дело, когда хакер, который особо не вникал в происходящее в коде, пытается нагадить, и совсем другое, когда нагадить хочет тот, кто этот код писал)
PaulIsh
Без чтения исходного кода, где собственно этот setByPath и используется было не понятно как произошло загрязнение. Можно было подумать на bodyParser, но оказывается там есть метод `app.put('/', (req, res) ...` где собственно всё и происходит.