
Предложение TC39 по смягчению прототипного загрязнения
Авторы: 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 можно реализовать с помощью полифиллов, что позволяет укреплённым кодовым базам работать во всех браузерах, включая старые версии.