December 11

React2Shell

перевод поста Гильермо Рауч

React2Shell

Гильермо Рауч

Опубликовано 6 декабря 2025 г.

Эта странная структура данных

ответственна за множество бессонных ночей на этой неделе по всей отрасли. Это теперь печально известная React2Shell, обнаруженная Лакланом Дэвидсоном, которая теперь широко распространяется.

Я хотел бы поделиться своей точкой зрения по поводу этой атаки, как она возникла, что мы делаем и какие уроки извлекли.

Эта уязвимость — настоящая головоломка. Сочетание вопиющего упущения в проверке безопасности с потрясающе умным механизмом эксплуатации. Решение напоминает задачку на сообразительность… загадку, проверяющую навыки хакера, или, как называют это люди в этом сообществе, упражнение «CTF» (захват флага).


Фон: JS, React, Flight

Сначала небольшое введение в JavaScript. JS славится своей высокой динамичностью. Типы динамические, а выполнение кода — тоже динамическое. Этот хак эксплуатирует эти две уникальные свойства языка, злоупотребляя его врожденной duck typing и прототипной природой, чтобы обмануть парсер React, и способностью JavaScript выполнять произвольный динамический код во время выполнения (или eval его), делая это самым серьезным классом уязвимости. CVE-2025-55182 в результате получает 10.0/10.0.

Давайте также разберемся с React. Уязвимость лежит в протоколе React Flight, который используется для кодирования входных и выходных данных для Server Functions и Server Components (RSC) React. Почему React нуждается в этом? Лучший способ думать о RSC — как о фреймворке «BFF» (Backend for the Frontend), не похожем на GraphQL. Чтобы делать сложные UI действительно быстрыми, React стримит сложные данные, соответствующие форме дерева рендеринга, за один хоп.

Flight — это протокол, который позволяет передавать эти данные туда-сюда. Он очень мощный и полный. Представьте, если бы JSON мог представлять данные, которые еще не готовы, например, Promise, чтобы ваш UI мог рендериться как можно быстрее, пока бэкенд или база данных еще не ответила. С большой силой приходит большая ответственность, так что Лаклан учуял беду.


Эксплойт

Чтобы объяснить эксплойт, я использовал POC от @maple3142

, который короче, чем у Лаклана. Maple был первым, кто эксплуатировал уязвимость через 30 часов после выхода патча. Тот факт, что публичный фикс был доступен в GitHub для всех, и что на его взлом ушло столько времени, — свидетельство открытия Лаклана, в которое он вложил 100 часов работы. Этот казалось бы невинный тело запроса потребовал дней неустанного исследования и проб и ошибок.

Примечание: трудно сказать, имел ли Maple «помощь» или нет. Поскольку мы сотрудничали с коллегами из отрасли, чтобы защитить интернет, произошла утечка ключевой подсказки, которая обретет смысл позже.

Мне удалось упростить POC Maple еще больше — насколько я знаю, это минимально жизнеспособный эксплойт:

{
  0: {
    status: "resolved_model",
    reason: 0,
    _response: {
      _prefix: "console.log('☠️')//",
      _formData: {
        get: "$1:then:constructor",
      },
    },
    then: "$1:then",
    value: '{"then":"$B"}',
  },
  1: "$@0",
}

Если это тело отправить на любой сервер, работающий с уязвимым кодом Flight, например, современные приложения Next.js, строка console.log('☠️') будет вычислена на сервере. Эта строка может делать практически что угодно. Запускать программы, извлекать секреты, делать сетевые вызовы. Это так плохо, как только может быть.

Два ключевых ингредиента эксплойта — 0 и 1 — называются «Chunks» в жаргоне Flight. Точка входа в эксплойт — ключевая возможность Flight стримить данные Promise. Синтаксис для этого — @ в 1: chunk $@0. Это говорит: 0 — это Promise.

Теперь: что за дело с этим resolved_model? Это гениальная часть эксплойта. Хотя Flight предназначен для транспортировки «объектов пользователя», Лаклан нашел способ запутать React. Он по сути выражает «внутреннее состояние» — объект, который должен быть приватным и содержать внутреннюю бухгалтерию React. Каждый раз, когда React должен представлять «в полете» объект, он использует внутреннюю структуру данных, которая отслеживает его статус. resolved_model означает, что его value готов к использованию.

Ключ к хаку — часть then. В JS, если объект «крякает как Promise», то есть имеет then, то это Promise. Поскольку мы использовали $@0, React в итоге «await» (.then()) эти данные. Чтобы злоупотребить этой машиной, мы устанавливаем then: "$1:then", который будет вызван рекурсивно. Здесь происходит вопиющее упущение Flight. Этот синтаксис : предназначен для ссылок между объектами пользователя, а не для доступа к внутренним механизмам JS-runtime.

Патч CVE вводит проверки hasOwnProperty, чтобы предотвратить этот доступ. Это основное упущение безопасности в React, фундаментальное, аналогичное выходу за границы памяти в C.

Как только мы обманываем React, заставляя разрешить этот chunk, он инициализируется с захваченным, фальшивым _response. Следующий ключ атаки — доступ к механизму оценки кода. Вы заметите, что атака использует значение, ссылающееся на $B, что означает Blob.

Почему именно Blob? В React есть строки:

const backingEntry = response._formData.get(blobKey);
return backingEntry;

Когда атакующий контролирует _formData и _prefix, остаётся лишь заставить .get оценивать код, а blobKey — предоставить его.

И здесь — последний ключ. В JS существует два основных механизма выполнения кода: eval и new Function. Но прямая их ссылка отсутствует. Зато есть: get: "$1:then:constructor". Обращаясь к then, мы получаем доступ к экземпляру Function, и затем к его constructor.

Финальный эксплойт семантически:

Function("console.log('☠️')//")()

Выводы и исправления

Это напоминание: нельзя доверять пользовательским данным. Особенно — что они не попытаются использовать внутренние механизмы JS (then, constructor) для захвата внутренних механизмов React (_response, status, и т.д.).

Мы выпустили патчи для React и Next.js и работали с коллегами по отрасли для быстрого внедрения мер защиты.

Файрволы могут помочь, но недостаточно — эксплойт слишком изощрённый. Vercel Firewall включает защиту для всех проектов, и программа вознаграждений

открыта для укрепления этих мер.

⚠️ Не пропускай обновление React и Next.js — установи патч немедленно.

Хочу поблагодарить Лаклана Дэвидсона за исследование и ответственное раскрытие. И всех коллег в сфере безопасности: @S1r1u5_, Hacktron AI, @infosec_au, Assetnote, а также команду Vercel.