从 React2Shell 学到的经验
Source: Dev.to
漏洞概述
该漏洞存在于 React 的 “Flight” 协议中,这是随 React Server Components 引入的自定义序列化格式。Flight 负责在客户端和服务器之间传输数据和执行上下文。该漏洞使攻击者能够构造恶意负载,当服务器对其进行反序列化时,可执行任意代码。攻击不需要身份验证——只要拥有网络访问权限,向任意 Server Components 端点发送精心构造的 HTTP 请求即可。
技术根本原因是 对不可信客户端数据的非安全反序列化。服务器接受来自客户端的序列化对象,反序列化后根据其内容执行代码,包括访问 .then、.constructor 等属性,这些属性暴露了 JavaScript 的代码执行原语。React 的防御依赖于序列化格式本身能够阻止恶意输入的假设,而不是默认将所有客户端数据视为不可信。
什么是 React Server Components?
React Server Components(RSC)代表了 React 架构的根本转变。传统上,React 是在浏览器中运行的客户端库,用于渲染用户界面并通过标准的 REST 或 GraphQL 端点与后端 API 通信。后端可以使用任何语言编写:Python、Go、Ruby、Java 等。
Server Components 改变了这一模型。它们允许 React 组件在服务器上执行,直接访问数据库,并使用 Flight 协议将结果(包括 Promise 和复杂状态)序列化后发送给客户端。标记为 'use server' 的函数会自动成为服务器端端点,无需显式的 API 路由。框架负责路由这些 “Server Actions” 并在客户端与服务器之间序列化数据流。
这种设想极具诱惑力:在同一文件中使用相同语言编写前端和后端,实现“无缝”的数据流。无需 API 样板代码,无需上下文切换,只需组件即可“神奇”地知道在客户端还是服务器上运行。
违反安全原则
React 抛弃了数十年积累的安全智慧。安全系统的基本原则很简单:永远不要信任客户端输入。每个成熟的框架和语言生态系统都通过痛苦的经验学到了这一点:
- Java 序列化漏洞 导致无数应用出现远程代码执行,最终促使出现废弃警告并建议避免对不可信数据进行反序列化。
- PHP 的
unserialize()成为数千次 WordPress 被攻破的攻击向量;社区现在将对用户输入的反序列化视为反模式。 - Python 的
pickle文档明确警告:“pickle 模块不安全。只能对可信的数据进行 unpickle。” - Ruby 的
Marshal也有同样的警告和漏洞历史。
React 构建了一个自定义序列化协议,将客户端数据反序列化为服务器执行上下文。Flight 协议必须比 JSON “更聪明”——能够序列化 Promise、闭包和复杂对象图。这种额外的复杂性本身就带来了危险。
该漏洞并非偶然的实现错误;它是违反基本安全原则的必然结果:对不可信数据进行复杂反序列化会导致远程代码执行。如果做不到完美,就不要去做。
传统的 REST API 通过使用 JSON 避免了这类漏洞,JSON 是一种刻意受限的数据格式,不携带执行上下文、代码或对象方法。JSON 在恰当的层面上是“愚蠢”的:它仅仅是数据结构。服务器接收 JSON,依据预期的 schema 进行验证,并显式路由到相应的处理器。不存在执行上下文的反序列化、客户端指定代码路径的自动调用,也没有数据与代码的模糊界限。
紧耦合:并非真正的 API
React Server Components 不仅带来安全风险,还削弱了架构灵活性。当你在函数上标记 'use server' 时,你并没有创建一个 API,而是创建了一个只能被使用 Flight 协议的 React 客户端调用的 React‑专属端点。
传统 REST API 示例(Python)
@app.post('/api/posts')
def create_post(data):
return db.posts.create(data)
该端点可以被以下对象调用:
- 你的 React 前端
- 你的移动应用(iOS/Android)
- 你的 CLI 工具
- 合作伙伴集成
- 第三方开发者
- 任何语言的 HTTP 客户端
- 如
curl或 Postman 等测试工具
它可以使用 OpenAPI/Swagger 进行文档化,使用标准 HTTP 工具进行监控,并通过常规 WAF 规则进行防护。
Server Action 示例(JavaScript)
'use server'
async function createPost(data) {
return await db.posts.create(data);
}
该函数只能被你的 React 前端调用。它使用的是只有 React 能理解的专有协议(Flight),无法以语言无关的方式进行文档化,标准的 HTTP 监控工具也无法解析其负载。安全工具无法检查流量。如果需要移动端,你仍然必须另建一个 REST API。
你并没有消除 API 样板代码,而是把它隐藏在框架的魔法背后,同时限制了使用者。当你的应用不可避免地需要支持多种客户端——网页、移动端、CLI——时,你将不得不维护两套平行系统:React Web 应用使用 Server Actions,其他所有场景使用正规 REST API。Server Components 的“便利”在需要与 React 生态之外的系统集成时立刻变成技术债务。
可复用性问题不仅限于多客户端。现代应用常常需要暴露 webhook、与合作伙伴 API 集成,或向分析平台提供数据——这些都无法消费 React Server Actions。于是你被迫重新构建传统 API,使 Server Actions 变得多余:一种为了解决不存在的问题而产生的方案,却带来了更多问题。
JavaScript 锁定:失去合适的工具
也许最隐蔽的危害在于 React Server Components 消除了架构选择的余地。过去 13 年里,React 可以配合任何后端。你的 API 服务器可以用 Python 进行数据科学,用 Go 提供高性能服务,用 Rust 做系统编程,用 Java 实现企业级集成,或用 Ruby 快速开发——选择权在于团队的专长和业务需求。
Server Components 从根本上改变了这一等式。要使用它们,服务器 必须是 JavaScript——具体来说是 Node.js 或兼容的运行时。Flight 协议、Server Actions 路由以及整个 Server Component 生态系统都与 JavaScript 运行时紧密耦合,迫使团队采用可能并非其领域最佳的技术栈。这种锁定降低了灵活性,增加了运维复杂度,并可能导致性能不佳或维护成本上升。