找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1132

积分

0

好友

164

主题
发表于 4 天前 | 查看: 88| 回复: 0

“React2Shell”是一位安全研究人员为 CVE-2025-55182 起的名字,意在致敬经典的 Log4Shell 漏洞。这是一个影响 React Server Components (RSC) 的严重安全漏洞。

CVSS评分: 10.0 (严重)
影响范围: React 19 全系列 + Next.js 15/16
漏洞性质: 无需认证的远程代码执行 (RCE)

CVE-2025-55182 是 React Server Components 中的一个满分 10.0 的严重远程代码执行漏洞,攻击者仅需发送单个精心构造的 HTTP 请求即可在无需认证的情况下实现服务器接管。该漏洞由安全研究员 Lachlan Davidson 于 2025 年 11 月 29 日发现,公开利用载荷由 maple3142 于 2025 年 12 月 5 日公开,影响 React 19 全系列及 Next.js、React Router 等下游框架。据 Wiz Research 数据显示,39%的云环境包含受影响实例,而利用测试显示成功率接近 100%。由于 React 在全球 JavaScript 框架市场占据 45.8%的份额,该漏洞的潜在影响覆盖数百万生产应用。

利用过程仅需一个特制的 HTTP 请求,并且在测试中显示出接近 100% 的可靠性。该漏洞的根源在于 RSC 负载处理逻辑中的不安全反序列化,使得攻击者可控的数据能够影响服务器端的执行逻辑。

影响版本

易受攻击的产品 补丁版本
**react-server-dom***: 19.0.0, 19.1.0, 19.1.1, 19.2.0 19.0.1, 19.1.2, 19.2.1
Next.js: 14.3.0-canary, 15.x, 16.x(应用路由) 14.3.0-canary.88, 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7

一、漏洞原理

1、什么是 Flight 协议?

Flight 是一种“在网络上传递 React 组件树信息”的协议,客户端用一个解码器把服务器返回的“帧”(frame)解析成可恢复的 React 结构,再进行局部更新和渲染。

这是 React 自己实现的一套协议,主要用途包括:

  • Server → Client:把 Server Components 生成的 UI 结果,序列化成一串「Flight 数据流」,通过 HTTP 传给浏览器,由客户端 React 按协议反序列化、拼成最终 UI。
  • Client → Server (Reply Flow):浏览器在执行 Server Actions 或交互时,把参数等信息按 Flight 格式编码,POST 回服务器,由 React 的解析器解包、找到对应的 Server Function 去执行。

经过协议处理后的内容具有以下特征:

  • Content-Type 通常为:text/x-component
  • $ 开头的特殊标签来表示“特殊值”
  • 每条记录有一个 id(行号或资源号)

实际上可以理解为:一个专门为 React 组件树和异步依赖设计的、半二进制的自定义序列化格式。但 React 官方从未将 Flight 格式作为公开标准文档化,并且明确声明它不稳定、可能随版本变更。正因如此,各框架(Next.js, React Router, Vite RSC, Parcel RSC 等)都直接依赖 React 的这部分实现,这也导致了如果 React 本身出现安全问题,依赖它的所有框架都会受到影响。

2、Flight 反序列化

Flight 协议的反序列化过程主要依赖于流式数据解析,通过不同的标记(如 $F$L$P 等)来恢复 React 元素、模块和函数引用。Flight 数据流本质上是一系列记录,每条记录包含三个主要部分:

  • 类型标记:标记该记录的数据类型,如组件、函数、模块、Promise 等。
  • ID 或引用:该记录对应的唯一标识符。
  • Payload (数据负载):包含实际的数据,如模块路径、函数参数、异步数据等。

这里需要区分两个方向的数据流:

  • Server → Client:服务端生成 Flight 流,发给浏览器。
  • Client → Server (Flight Reply):浏览器把 Server Action 的调用和参数编码成 Flight Reply,发回服务端,由 decodeReply / decodeReplyFromBusboy 解包。

真正与此次漏洞相关的是 Client → Server (Flight Reply) 方向。服务端通过 decodeReply/decodeReplyFromBusboy 把来自客户端的 Flight payload 反序列化成 JavaScript 对象或函数。

典型的文本/urlencoded 场景使用 decodeReply

// 文本 / urlencoded 场景:使用 decodeReply
import { decodeReply } from 'react-server-dom-webpack/server';

app.post('/action', async (req, res) => {
  // 伪代码:把请求体读成字符串 / Buffer
  const body = await getRawBody(req);
  // 由 React 负责把 Flight Reply 反序列化成参数数组
  const args = await decodeReply(body, webpackMap);
  // 通常 args[0] 是要调用的 Server Action,后面是参数
  const [action, ...actionArgs] = args;
  const result = await action(...actionArgs);
  res.json(result);
});

在 Next.js / RSC 典型的 multipart/form-data 场景里,会配合 Busboy 使用 decodeReplyFromBusboy

import busboy from 'busboy';
import { decodeReplyFromBusboy } from 'react-server-dom-webpack/server';

app.post('/action', (req, res) => {
  const bb = busboy({ headers: req.headers });
  // 把 Busboy 作为流传给 React,返回一个 thenable
  const reply = decodeReplyFromBusboy(bb, webpackMap);
  // 原始 HTTP 请求流喂给 Busboy
  req.pipe(bb);
  reply
    .then(async (args) => {
      const [action, ...actionArgs] = args;
      const result = await action(...actionArgs);
      res.json(result);
    })
    .catch((err) => {
      res.status(500).end(err.message);
    });
});

decodeReply/decodeReplyFromBusboy 内部就是 Flight 反序列化逻辑的核心,它会按 $L / $@ 等标记,把记录解析成函数引用、模块引用、Promise 等对象。

3、一个抽象示例

假设有一套安全的“Server Actions”供前端调用:

// server/actions.js
async function addTodo(text) { /* ... */ }
async function deleteTodo(id) { /* ... */ }
export const actions = { addTodo, deleteTodo };

服务器端的安全反序列化逻辑大致如下:

app.post('/flight', async (req, res) => {
  const payload = JSON.parse(req.body);
  // 只处理 type='action'
  if (payload.type !== 'action') return res.status(400).end('bad type');
  // 只允许调用「预先注册在 actions 映射」里的函数
  const fn = actions[payload.id];
  if (typeof fn !== 'function') return res.status(400).end('unknown action');
  // 参数当作普通数据使用
  const result = await fn(...payload.args);
  res.json({ ok: true, result });
});

这里的安全边界清晰:类型固定、ID 必须在白名单内,后端不会根据客户端数据去动态 require() 任意模块或执行 eval

现在,假设为了支持更“灵活”的特性(如模块引用),改造协议并实现一个“通用反序列化器”:

app.post('/flight', async (req, res) => {
  const payload = JSON.parse(req.body);
  if (payload.type === 'action') { /* ... 安全处理 ... */ }
  if (payload.type === 'callModule') {
    // 把「客户端字符串」当模块名和导出名来 require / 调用
    const mod = await import(payload.module);
    const fn = mod[payload.export];
    if (typeof fn !== 'function') return res.status(400).end('not a function');
    const result = await fn(...payload.args); // 危险!
    return res.json({ ok: true, result });
  }
  res.status(400).end('unknown type');
});

此时,攻击者可以控制 payload.modulepayload.export,如果项目中存在敏感函数,就可能被滥用,从而演变为 RCE、SSRF 或任意文件读写漏洞。

抽象回 React Flight 反序列化:问题就出在把功能强大的解码器用在了不可信的客户端输入上。Flight 协议在 Server → Client 方向是安全的,因为数据源是可信的服务器。但在 Client → Server 方向,同一套能理解“模块引用/函数引用”的强解码逻辑被复用,却没有对“哪些标记允许客户端触发”进行严格限制。一旦解码路径会执行模块加载(import())、动态方法调用(obj[客户端字符串])或把解析出的对象传递给敏感 API,就形成了高危的反序列化漏洞入口。

4、React Flight 反序列化流程

这里说的反序列化流程特指服务端处理 Flight Reply 的流程。其关键步骤简化如下:

HTTP POST 请求 (multipart/form-data)
  ↓
decodeReply(formData, serverReferenceMap)
  ↓
createServerResponse() // 创建响应对象
  ↓
parseReply(formData)   // 解析 FormData
  ↓
  每个字段:
    ↓
    initializeModelChunk(json)
      ↓
      JSON.parse(json, reviveModel)
        ↓
        parseModelString(value) // 核心解析函数
          ↓
          处理特殊前缀:
          - $F → createBoundServerReference (验证安全性)
          - $T → readTemporaryReference (Proxy 保护)
          - $B → 获取 Blob
          - $Q, $W → 构建 Map/Set
          - $123 → getOutlinedModel (解析引用)
  ↓
返回完整的 JavaScript 对象/函数

核心函数 parseModelString 负责解析和验证所有字符串类型的值,其主要逻辑包括:

  • 所有特殊值都必须以 $ 开头。
  • 对于 $$ 开头的值,视为转义字符串。
  • 对于 Promise 类型 ($@),解析十六进制 ID 并获取对应的 chunk。
  • 服务端引用 ($F) 需要通过 getOutlinedModel 获取元数据,并加载相应的服务端函数。
  • 临时引用 ($T) 会进行安全检查。
  • 为各种特殊 JavaScript 值(如 Infinity, NaN, Date, BigInt)提供安全的反序列化。

关键在于,只要传入的数据符合 Flight 所规定的数据类型和格式,就可以反序列化成功,但早期的实现在安全方面考虑不足。

5、Patch 补丁分析

关键的修复补丁位于 facebook/react@e2fd5dc,主要涉及以下几点:

第一处:reviveModel 函数

- if (newValue !== undefined) {
+ if (newValue !== undefined || key === '__proto__') {
    value[key] = newValue;
  }

即使 newValueundefined,如果键是 __proto__,也强制进行赋值操作,防止原型链被恶意修改。

第二处:preloadModule / requireModule 函数
引入了 hasOwnProperty.call() 来确保只访问对象的自有属性,完全隔离原型链访问。

第三处:新增 fulfillReference 和 修改 getOutlinedModel 函数
在路径遍历 (value = value[name]) 前,增加了严格的属性存在性检查,防止通过路径遍历访问到危险的原型链属性。

// 修复后
if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
  value = value[name];
}

通过分析补丁可以理解漏洞利用的大致流程:通过 Flight 的反序列化功能,传入带有原型链污染的数据,在某个原型链属性的调用后,实现 RCE。

二、漏洞分析

1、已有信息分析

漏洞的本质是一个原型链污染导致的 RCE。原型链污染本身通常只能“修改配置”,要升级为 RCE,关键在于能否将被污染的“配置”转换为“代码执行的参数或函数”。这通常需要程序存在能从字符串或配置中动态选择并调用模块或函数的机制(即 gadget),且该机制的配置来源于一个容易被污染的对象。

在 React2Shell 漏洞中,Server Action 的数据入口是 decodeReplyFromBusboy,它解析 multipart/form-data 数据。数据经过 resolveField -> resolveModelChunk -> initializeModelChunk -> JSON.parse,最终到达关键的 reviveModel 函数。

reviveModel 中,如果值是字符串(如 "$F1"),会调用 parseModelString 处理特殊标记。对于服务端引用 ($F),会进一步调用 getOutlinedModel 来解析引用路径。

关键的漏洞点在于 getOutlinedModel 函数中的路径解析逻辑(修复前):

let value = chunk.value;
for (let i = 1; i < path.length; i++) {
  const name = path[i]; // 可控点
  value = value[name]; // 修复前:直接访问,可能访问原型链
}

如果攻击者构造一个引用路径如 "$F1:__proto__:constructor",那么解析过程将是:

path = ["1", "__proto__", "constructor"]
value = chunk[1].value; // 假设为普通对象
value = value["__proto__"]; // 访问到 Object.prototype
value = value["constructor"]; // 访问到 Function 构造函数

这就为后续的代码执行提供了关键的 Function 构造器。

2、寻找一个 Gadget

仅获得 Function 构造函数还不够,还需要一个调用它的“契机”(call gadget)。公开的 PoC 展示了一个极其巧妙的构造:Chunk 套 Chunk

简化后的攻击载荷结构如下:

name="0": {
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "value": "{\"then\":\"$B\"}",
  "_response": {
    "_prefix": "恶意JS代码",
    "_formData": {"get": "$1:constructor:constructor"}
  }
}
name="1": "$@abc" // 指向一个不存在的 chunk[2748]

其核心思路是:

  1. 污染 _formData.get:通过路径 $1:constructor:constructor,最终使 response._formData.get 指向全局的 Function 构造函数。
  2. 设置恶意代码:将待执行的恶意 JavaScript 代码字符串放入 _response._prefix
  3. 构造触发点:在 value 字段中放入 {"then":"$B"}。当解析 $B (Blob) 标记时,会执行以下逻辑:
    case 'B': {
      const id = parseInt(value.slice(2), 16);
      const prefix = response._prefix; // 恶意代码
      const blobKey = prefix + id;
      const backingEntry = response._formData.get(blobKey); // 此时 get 已是 Function
      return backingEntry; // 返回一个新函数:Function(恶意代码)
    }
  4. 函数调用:这个新函数被赋值给一个对象的 .then 属性。在 React 内部处理 Promise/thenable 链时,这个 .then 方法最终会被调用,从而执行恶意代码。

这个 Gadget 链的精妙之处在于,它利用了 Flight 协议内部对未解析 chunk 的等待机制(通过 $@ 引用一个永不存在的 chunk),巧妙地串联起了原型链污染、函数构造和调用触发整个流程。

3、漏洞修复分析

补丁从多个层面封堵了利用路径:

  1. 路径遍历安全:在 getOutlinedModel 中增加了 hasOwnProperty 检查,阻止通过 __proto__ 等属性访问原型链。
  2. 属性赋值安全:在 reviveModel 中,将 __proto__ 视为普通属性,阻止其污染全局原型。
  3. 模块加载安全:在 requireModule 中使用 hasOwnProperty,确保只加载模块自身的导出。

当前的修复基本阻断了已知的 RCE 利用链。但是,Flight 协议本身依然非常复杂,其反序列化逻辑将客户端字符串转换为丰富的 JavaScript 对象(包括函数引用、模块引用等)。只要这种强大的能力存在,并且应用于处理不可信的客户端输入,理论上就存在出现新逻辑缺陷的可能。此外,如果业务代码对反序列化得到的对象进行不安全的深度合并或配置合并,仍可能在应用层引入新的原型链污染问题。

三、总结

  1. 影响范围:并非所有使用 React 的项目都会受影响。该漏洞主要影响使用了 React Server Components (RSC) 的项目,尤其是基于 Next.js(应用路由器)构建的应用。传统的 SPA + API 前后端分离项目通常不受影响。
  2. 漏洞本质:这是一个由于 React Flight 协议反序列化器功能过于强大,且应用于不可信客户端输入而导致的漏洞。攻击者通过精心构造的 Flight Payload,利用路径遍历进行原型链污染,最终结合内部 Gadget 实现远程代码执行。它并非典型的向 Object.prototype 直接注入属性的全局污染。
  3. 修复现状:官方补丁已修复了导致 RCE 的关键路径。开发者应立即升级 React 和 Next.js 到安全版本。
  4. 安全启示:对于实现自定义复杂序列化/反序列化协议的系统(尤其是在像 Node.js 这样的服务器端环境中),必须对客户端输入的解析能力施加严格的限制和白名单控制。在安全开发实践中,应避免将用于处理可信数据的强大解析器直接用于处理不可信的外部输入。



上一篇:Rust跨平台编译实战:Eurydice工具链与AWS云应用、死锁调试及自研语言挑战
下一篇:SpecFormer新范式优化LLM推测解码,Qwen3-4B大批量推理加速1.56倍
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 18:47 , Processed in 0.160483 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表