![]() |
https://forum.antichat.xyz/attachmen...5432563235.png
Prototype Pollution редко выглядит как громкая уязвимость. Обычно всё начинается с куска кода, который никто не считает опасным: функция слияния объектов, разбор параметров в структуру настроек, универсальный helper для deep copy, пустой config-объект, который потом уходит в рендер, загрузчик скрипта, шаблонизатор или системный вызов. До первого инцидента это воспринимается как нормальная инженерная обвязка. Проблема в том, что ошибка живёт не в одном конкретном месте. Один фрагмент принимает данные, другой пишет по ключу, третий позже читает унаследованное свойство как штатный параметр. В итоге баг возникает не на уровне одной функции, а на стыке нескольких безобидных решений. Этим Prototype Pollution и неприятна. По отдельности JSON.parse(), Object.assign(), разбор location.hash, рендер компонента или вызов child_process могут выглядеть совершенно спокойно. Но если пользовательское значение однажды доехало до прототипа, приложение начинает работать с чужими свойствами как со своими. На клиенте это выливается в DOM XSS, подмену src, srcdoc, обработчиков и конфигурации виджетов. На сервере - в подмену параметров фреймворка, шаблонизатора, промежуточного обработчика или системного API. Именно поэтому Prototype Pollution плохо ловится интуицией: источник находится в одном слое, эффект проявляется в другом, а между ними часто лежит обычный служебный код, который годами не вызывал подозрений. Эта уязвимость редко живёт как один баг в одном месте. Обычно это цепочка из трёх частей:
https://forum.antichat.xyz/attachments/4951404/1.png Где ломается модель объектов Почему пустой объект не пустой У обычного объекта в JavaScript есть собственные поля и прототип. Когда код читает свойство, движок сначала ищет его в самом объекте, потом поднимается по цепочке прототипов. Поэтому пустой {} на деле не совсем пустой - он наследует поведение от Object.prototype. Это нормальная механика языка. Проблема начинается, когда приложение даёт записать данные не в сам объект, а в его прототип. Тогда новые объекты начинают наследовать уже не только штатные свойства, но и то, что туда кто-то подложил. Если загрязнён Object.prototype, эффект расходится по большому куску приложения сразу. Если меняется прототип конкретного объекта настроек, масштаб меньше, но для эксплуатации этого часто уже хватает. Уязвимости не обязательно нужно испортить весь рантайм. Иногда достаточно сломать один объект, который потом уйдёт в чувствительный код. proto, prototype и constructor.prototype На этой теме часто спотыкаются даже те, кто уже сталкивался с Prototype Pollution. Код:
__proto__prototype - свойство функции-конструктора, из которого создаются прототипы экземпляров. constructor.prototype - обходной путь к тому же объекту. https://forum.antichat.xyz/attachments/4951404/2.png Откуда берётся Prototype Pollution Небезопасное рекурсивное слияние Самый частый источник - самописное глубокое слияние объектов. Код выглядит настолько обычным, что его почти не замечают на ревью. JavaScript: Код:
functionJavaScript: Код:
constЭто и есть одна из причин, почему баг живёт так долго. Разработчик смотрит на JSON.parse() - там всё спокойно. Смотрит на deepMerge() - тоже вроде без криминала. Проблема рождается на стыке. Параметры URL и разбор пути Во фронтенде источник pollution часто сидит в коде, который вообще не воспринимается как опасный. Например, страница умеет превращать query string во вложенный объект настроек. JavaScript: Код:
functionНа ревью такие места проще узнавать по трём признакам сразу:
Цитата:
Во фронтенде опасны не только HTML-вставки. Нередко загрязнение доезжает до загрузки скриптов. JavaScript: Код:
functionJavaScript: Код:
constОбработчики событий и поля обратного вызова Есть ещё один класс гаджетов, который часто упускают. Приложение не пишет в DOM напрямую, но использует значение из объекта настроек как функцию, имя обработчика, строку для таймера или параметр инициализации сторонней библиотеки. Можно так: JavaScript: Код:
functionJavaScript: Код:
functionКак это выглядит в коде SPA Если свести типовой клиентский сценарий к минимуму, получится что-то такое: JavaScript: Код:
function
Server-side: где начинается опасная часть На сервере Prototype Pollution неприятнее по двум причинам. Во-первых, загрязнение живёт в процессе до перезапуска. Во-вторых, даже аккуратная проверка может случайно превратиться в отказ в обслуживании, если задеть не тот путь и не тот гаджет. Поэтому для серверной части полезно начинать не с попыток сразу добраться до выполнения команд, а с безопасных индикаторов - таких, которые меняют наблюдаемое поведение сервиса, но не ломают его. Безопасный индикатор на Express Ниже минимальный пример, где pollution сначала попадает в процесс, а потом проявляется в другом обработчике. JavaScript: Код:
constЭто очень полезный практический кусок по двум причинам. Во-первых, он показывает server-side pollution без опасной операционки. Во-вторых, сразу видно главное отличие от клиентской части: загрязнение не исчезает после одного рендера. Оно остаётся жить в процессе и начинает влиять на последующие запросы. Почему child_process становится гаджетом Теперь можно переходить к серверным эффектам посерьёзнее. Опасность child_process не в том, что разработчик обязательно запускает внешнюю команду с пользовательской строкой. Для Prototype Pollution это даже не нужно. Проблема начинается там, где код передаёт в API запуска процесса пустой или неполный объект опций и рассчитывает, что недостающие поля просто останутся не заданными. JavaScript: Код:
constШаблонизаторы и скрытая конфигурация С шаблонизаторами история похожая. Проблема обычно не в том, что приложению напрямую подсовывают произвольный шаблон. Проблема в том, что в render(), compile() или renderFile() передаётся объект настроек, часть которого должна была быть пустой или безопасной по умолчанию. JavaScript: Код:
functionОсобенно больно это находить в больших Node.js-приложениях, где шаблонизатор обёрнут несколькими внутренними слоями, а источник pollution сидит вообще в другой части сервиса. Цитата:
Для клиентской части хорошо работают инструменты, которые умеют быстро находить источник pollution и проверять, доезжает ли значение до опасного места в DOM. Здесь особенно полезны DOM Invader и похожие средства для анализа клиентских цепочек. Для первичного скрининга встречаются и более узкие утилиты вроде ppmap. Для серверной части автоматизация полезна, но не стоит ждать от неё чуда. Наружный скан может подсветить безопасные индикаторы - странный код ответа, поведение JSON, неожиданные заголовки. Настоящая ценность обычно появляется при разборе зависимостей, обвязок над child_process, шаблонизаторов и функций слияния данных. Инструменты здесь экономят время, но не заменяют понимание формы бага. Prototype Pollution слишком часто живёт в стыках между слоями, чтобы её можно было полностью отдать одному сканеру. Как чинить это без косметики https://forum.antichat.xyz/attachments/4951404/3.png Не делать “универсальное” глубокое слияние пользовательских объектов Первое и самое полезное исправление - перестать бездумно вливать произвольный пользовательский объект в структуру приложения. Вместо “умного” общего deepMerge() почти всегда лучше работает явное извлечение разрешённых полей. JavaScript: Код:
functionНе использовать обычные объекты как словари Если нужна структура “ключ-значение” с пользовательскими ключами, лучше брать Map или объект без прототипа. JavaScript: Код:
constJavaScript: Код:
constПроверять принадлежность поля самому объекту Если приложение читает необязательные свойства у объекта настроек, полезно проверять, что поле реально принадлежит самому объекту, а не прототипу. JavaScript: Код:
functionПочему заморозка прототипа не поможет Идея с Object.freeze(Object.prototype) регулярно всплывает как быстрое средство защиты. Как дополнительный барьер она полезна, но не решает архитектурную проблему сама по себе. У неё есть цена: совместимость, порядок инициализации, поведение полифилов, неожиданные побочные эффекты в старом коде. Если приложение уже массово опирается на универсальные merge helper’ы, пустые объекты настроек и динамическую запись по путям, заморозка прототипа будет скорее аварийным тормозом, чем нормальным исправлением. Вместо заключения Как только в приложении есть путь записи в прототип и участок кода, который читает несуществующее свойство как допустимое значение по умолчанию, дальше уже начинается не “теоретическая особенность JavaScript”, а вполне рабочая поверхность атаки. На клиенте она уходит в DOM XSS и подмену загрузки. На сервере - в управление поведением процесса, фреймворка и системных вызовов. Хорошая защита начинается не с поиска одного запретного ключа. Она начинается там, где команда перестаёт воспринимать универсальный объект как безопасный контейнер для всего подряд. |
| Время: 09:36 |