
TC39 proposal for mitigating prototype pollution
Авторы: Santiago Díaz (Google)
Куратор: Shu-yu Guo (Google)
Стадия: 1
freeze, seal и preventExtensions
Это предложение направлено на смягчение уязвимости уровня языка, известной как prototype pollution, с помощью механизма, дополняющего примитивы заморозки, и механизма, делающего с ним совместимым большинство кодовых баз. В нём описывается opt-in функция, которая делает прототипы доступными только через reflection API. Благодаря этому выражение obj[key] больше не может получить доступ к прототипам. Кодовые базы, совместимые с этой функцией, становятся более осознанными в том, как они используют прототипы.
Уязвимости PP позволяют атакующим манипулировать объектами, которыми они не управляют или к которым у них нет доступа во время выполнения. Этот примитив «жуткого дальнодействия» может использоваться для изменения формы других объектов и переопределения их свойств, тем самым отравляя объекты в рантайме.
Отравленные объекты разрушают базовые предположения кода, который в противном случае был бы безопасным/корректным, и могут приводить к произвольному выполнению кода и широкому спектру других проблем безопасности в JS-кодовых базах. Ошибки prototype pollution часто проявляются в веб-приложениях, но также затрагивают и не-веб JS-рантаймы.
Свойства объектов в JS доступны для записи любому коду, который может на них ссылаться. В частности, если множество объектов полагается на общее свойство, любой из них может вносить изменения во все остальные.
Особое свойство PP состоит в том, что это data-only атака, позволяющая достичь выполнения кода чисто через данные. Например, рассмотрим следующий уязвимый код и соответствующий эксплойт:
// source is attacker-controlled
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if(target[key] === undefined) {
target[key] = {};
}
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// User input comes as a string
const userSuppliedObj = JSON.parse('{"__proto__": {"polluted": true}}');
// Trigger prototype pollution
merge({}, userSuppliedObj);
// Create a brand new object
const newObj = {};
// Has polluted property
console.log(newObj.polluted); // true
Обратите внимание, что эксплойт способен отравить создание новых объектов, не внедряя никакого постороннего кода.
Из-за этого особого свойства современные средства смягчения проблем с выполнением кода — такие как Content Security Policy или Trusted Types — не способны защитить от PP, поскольку они сосредоточены на обеспечении происхождения кода.
Примечание: data-only атаки актуальны в ситуациях, когда код, выполняющийся на VM, является доверенным, а произвольное выполнение кода имеет последствия для безопасности.
freeze, seal и preventExtensionsСуществующие примитивы заморозки страдают от серьёзных проблем дизайна, из-за которых их вряд ли широко примут. Они могут быть полезны экспертам, но не подходят для применения большинством разработчиков, которые обоснованно ожидают, что прототипы будут изменяемыми:
Freeze API страдают от ошибки override и других несогласованностей, которые вносят баги в существующие кодовые базы, заставляя их выбрасывать исключения или, что хуже, молча выходить из строя в нестрогом режиме. Предыдущее исследование ошибки override показало, что ошибка override срабатывает примерно в 10% кодовых баз в строгом режиме и в 20% в нестрогом режиме. Вскоре после этого исследование было свёрнуто.
Freeze API возлагают на разработчиков тяжёлую ответственность за знание того, какие прототипы следует замораживать для поддержания безопасной кодовой базы, предполагая, что разработчики являются экспертами по безопасности. Эти API описывают что, но не как в безопасности. Замораживания Object, безусловно, недостаточно, поскольку многие эксплойты злоупотребляют Array. А как насчёт Error, Date, Reflect или Proxy? Или будущих встроенных типов? Freeze API не дают ответов на эти вопросы.
Freeze API предполагают стабильную точку заморозки: фиксированный момент во время выполнения, когда прототипы устоялись и их можно заморозить. На практике эта точка нестабильна и со временем меняется в активно разрабатываемых кодовых базах. Хотя сегодня во многих приложениях можно найти такую точку, добавление новых зависимостей, полифиллов, изменение структуры кода и такие мощные функции, как горячая замена и инструменты разработчика, превращают точки заморозки в движущуюся цель.
Freeze API не могут защитить всю цепочку прототипов. В JS объекты могут быть добавлены в цепочку прототипов или удалены из неё в любой момент времени. Чтобы защитить всю цепочку, нужно всегда помнить о заморозке объектов, добавляемых в цепочку, — это процесс, подверженный ошибкам. Когда они удаляются из цепочки, их уже нельзя разморозить.
В двух словах: функция, которая открывает прототипы только через reflection API. Если бы прототипы не были доступны через такие свойства, как __proto__ или prototype, они не были бы подвержены data-only проблемам.
Это лучше понять на примере: выражение obj[one][two] = value уязвимо к PP через obj.__proto__.polluted. Если удалить свойство Object.prototype.__proto__, то то же выражение больше не уязвимо, поскольку не может использовать единственный другой способ добраться до прототипов, а именно obj.constructor.prototype.polluted. Обратите внимание, что прототип нельзя удалить.
Это предложение может быть реализовано путём предоставления reflection API и создания новой опциональной функции инкапсуляции, которая удаляет свойства прототипов. Ниже приводится описание каждого шага.
__proto__ — это устаревшее имя свойства, которое можно удалить, но внутренний слот за ним по-прежнему можно читать через Object/Reflect.getPrototypeOf и записывать через Object/Reflect.setPrototypeOf; это просто продолжит делать свойство доступным для уже выполняющегося кода.
Мы предлагаем создать новые API для prototype, например getClassPrototypeOf и setClassPrototypeOf, которые позволят удалить это имя свойства, никак не меняя того, как это особое свойство работает и поддерживает VM.
Reflection API можно реализовать с помощью полифиллов, что позволяет укреплённым кодовым базам работать во всех браузерах, включая старые версии.
Новая опциональная «функция инкапсуляции», при которой для функций-геттеров и функций-сеттеров слотов прототипов не создаются имена свойств; теперь это возможно, поскольку вместо ссылок на эти свойства можно использовать reflection API.
Функция включается с помощью внеполосного флага:
X-Encapsulate-Prototype: true--encapsulate-prototypeКогда инкапсуляция отключена, прототипы доступны как через свойства, так и через reflection API.
Когда инкапсуляция включена, прототипы доступны только через reflection API, поскольку удалены и __proto__, и prototype.
Инкапсуляция также включает следующую функцию автоматического рефакторинга:
Когда инкапсуляция включена, JS-движки при загрузке нового исходного кода добавляют дополнительный шаг в фазу разбора, который регистрирует все обращения через точечную нотацию к свойствам прототипов так, как если бы это были вызовы соответствующих reflection API. Этот шаг может быть реализован эффективно и позволяет кодовым базам с сторонними, транзитивными или динамически загружаемыми зависимостями быть совместимыми с инкапсуляцией.
В будущем это изменение подготовит почву для пометки prototype как устаревшего.
Свойства прототипов при включённой инкапсуляции могут быть просто undefined, но они могут и выбрасывать ошибку при попытках чтения/записи. Это означало бы быстрый и громкий отказ и позволило бы тестировать миграцию на reflection API/инкапсуляцию.
Это подразумевает сделать геттеры и сеттеры __proto__ и prototype условными в зависимости от инкапсуляции, используя хук хоста, аналогично тому, как функция eval выбрасывает исключение при Content Security Policy.
Код, который полагается на вычисляемый доступ к свойствам для обращения к прототипам, несовместим ни с инкапсуляцией, ни с автоматическим рефакторингом. Его необходимо отрефакторить, чтобы явно обращаться к прототипам при их использовании. Такой рефакторинг фактически заставляет код выражать намерение, что делает опасные паттерны видимыми для статического анализа. На практике кодовые базы с такой характеристикой — это обычно reflection-фреймворки, инструменты отладки и другие сценарии активного использования reflection, которые, скорее всего, осознают, как они используют прототипы.
Кодовые базы, использующие слово prototype для определения пользовательских свойств, несовместимы. Такие кодовые базы можно сделать совместимыми с инкапсуляцией, если это свойство всегда устанавливается/читается через скобочную нотацию. Исторически, согласно запросам HTTP Archive, существует небольшой процент кодовых баз, несовместимых по этой причине.
Некоторые изменения свойства constructor также могут обладать эффектом «жуткого дальнодействия». В ходе нашего исследования мы не нашли практических уязвимостей, затронутых этим.
Планка для успешности этой атаки значительно высока: как и в PP, нужно найти приложение с гаджетами, позволяющими и записывать, и читать произвольные свойства. Но в constructor pollution читающий гаджет должен читать из constructor.polluted, а не из polluted. Это резко сокращает число полезных гаджетов.
Некоторый минифицированный JS может быть несовместим с режимом инкапсуляции, поскольку статический доступ к свойствам может быть минифицирован в вычисляемый доступ. Мы обратились к HTTP Archive, чтобы получить оценку этого на практике. Следующая таблица показывает, что страницы с таким поведением стабильно ниже 1% в течение последних 12 месяцев для всех страниц, просканированных десктопным браузером:
Google наблюдает восходящий тренд в количестве багов, поступающих в нашу программу вознаграждения за уязвимости: 1 в 2020 году, 3 в 2021 и 5 на данный момент в 2022. Мы также выявили ещё несколько в ходе наших внутренних исследований.
Примеры уязвимостей включают:
Мы ожидаем, что число уязвимых приложений будет расти по мере развёртывания JavaScript-приложений в большем количестве сред (например, Electron, Cloudflare Workers и т.д.). Поэтому для смягчения атак во всех средах требуется решение на уровне языка.
| Таблица | Документы, динамически обращающиеся к __proto__ или constructor | Всего просканировано документов | Доля |
|---|
| 2023_03_01_desktop | 5,407,936 | 609,469,458 | 0.89% |
| 2023_02_01_desktop | 4,842,383 | 549,089,708 | 0.88% |
| 2023_01_01_desktop | 5,283,826 | 589,519,160 | 0.90% |
| 2022_12_01_desktop | 5,161,471 | 577,073,883 | 0.89% |
| 2022_11_01_desktop | 5,023,169 | 561,726,239 | 0.89% |
| 2022_10_01_desktop | 4,393,377 | 476,880,624 | 0.92% |
| 2022_09_01_desktop | 4,239,257 | 466,278,762 | 0.91% |
| 2022_08_01_desktop | 4,259,814 | 463,784,047 | 0.92% |
| 2022_07_01_desktop | 3,011,137 | 339,468,615 | 0.89% |
| 2022_06_01_desktop | 2,301,317 | 257,501,222 | 0.89% |
| 2022_04_01_desktop | 2,368,577 | 263,144,657 | 0.90% |
| 2022_03_01_desktop | 2,319,518 | 259,249,013 | 0.89% |