React2Shell 漏洞事件

Hacker News Top 新闻

摘要

安全研究员 Lachlan 于 2025 年 11 月 30 日发现并报告了一个名为“React2Shell”的严重远程代码执行漏洞,该漏洞存在于 React 服务器组件协议中,并向 Meta 进行了报告。Meta 于 12 月 3 日发布了修复程序和安全公告(CVE-2025-55182),敦促开发者立即更新,因为该漏洞影响了数百万使用 React/Next.js 构建的网站。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/09 00:32

# React2Shell 漏洞发现始末 来源: https://lachlan.nz/blog/the-react2shell-story/ 2025年11月30日,我向 Meta 报告了一个严重的远程代码执行漏洞("React2Shell")。12月3日,Meta 发布了修复方案和公开通报(CVE-2025-55182),敦促开发者立即更新。 有趣的是,我最初的目的并不是要在 React 中寻找漏洞。我只是想理解一个协议,以便更好地学习如何攻击现代 Web 应用。但最终,我掉进了一个深坑,发现了一个影响数百万网站的严重漏洞。 我还建议阅读 Sylvie 关于 React2Shell 的博客文章,以及随后发生的一系列有趣事件。(https://sylvie.fyi/posts/react2shell/) > 本文中的日期以 NZDT(GMT +13)显示。这与 Sylvie 的文章(GMT -7)和 Meta 的(GMT -8)形成对比。 ## 周一 出发 作为职业黑客,2025年11月24日星期一的开始和往常一样:完成报告、启动新项目等。但那天下午,在好奇心和挫败感的驱使下,我感到大脑中有什么东西被触发了,于是我一头扎进了一个兔子洞,再也没有回头。 ### 一些背景 近年来,我渗透测试过很多基于 Next.js 构建的 Web 应用——这是一个非常流行的 React 框架。Next.js 使用 React Server Components(RSC)在服务器上高效渲染内容并发送到用户的浏览器,还使用 React Server Functions(之前的 Server Actions)让用户交互能够无缝调用服务器端的 JavaScript 代码。 Server Actions 刚推出时被很多人嘲笑(你可能记得这张广为流传的图片?),但它确实是一个相当酷的功能,逐渐流行起来。在一个代码库中,开发者可以编写服务器端代码并从客户端代码调用它。 为了实现这些功能,你的浏览器和服务器需要一种奇特的的方式来来回回发送消息,而现有技术并不太适合这种情况。因此,React 团队必须构建一些新的东西。 任何渗透测试过使用 Server Functions 的 Web 应用的人都会熟悉这种稍微奇怪的请求格式: ``` 0=[{ "a": "$$undefined", "b": "$1:foo:bar" }]&1=... ``` 作为整个行业,我认为我们都会想"嗯,这只是加了花哨功能的 JSON",然后像测试传统应用一样测试它,尽管这里显然有更多的攻击面。我当时确实也是这样做的。 ### 开关被触发的那一天 在这个星期一,我产生了强烈的欲望去了解这个格式。我*必须*了解它。认识我的人可以证明,我做事从来不会半途而废,会竭尽全力把一个问题研究透彻。大多数 Web 应用黑客攻击只是在向应用投掷开发者没有预料到的东西。如果这个协议能给我更多独特的攻击 Web 应用的方法,我*必须*了解它;我甚至可以为它建立一个完整的方法论! ### 等等,这到底是什么? 于是我开始查看文档——哦……没有关于这个协议的规范,现有的文档也少得可怜。这个协议到底叫什么名字?我花了不少功夫才发现它的名字叫"Flight"。现在很容易找到*了*,但在 React2Shell 披露之前,令人惊讶的是连名字都很难找到,更别说协议的格式了。我能找到的最好信息只是 X 上一些讨论 RSC 的帖子。 我已经被理解 Flight 深深吸引,但看到"没有文档,只有代码"更是激起了我的斗志。我的命运已经注定;不成为 Flight 专家我绝不罢休。我几乎没有睡过觉,到了第二天早上,我对协议的基本原理有了很好的理解,但这只是开始。 ### Flight 101 Next.js 允许开发者神奇地在客户端和服务器端代码之间传递复杂的 JavaScript 对象,但这包括无法用简单 JSON 表示的对象。Flight 解决了这个问题,添加了对更复杂数据类型(如 `Date`、`BigInt` 和 `Map`)、引用(包括循环引用)和 Promise(异步到达的数据)的支持,等等。它仍然基于 JSON,但 Flight 消息被分解为"数据块"。每个数据块通常作为表单元素发送,可以异步到达,可能无序到达。特殊的 `$` 语法表示 Flight 类型。如下所示,`$D` 表示日期,`$x` 是对另一个数据块的引用,`$x:y` 允许属性选择。 ``` 0={"email":"[email protected]","updated":"$D04 Dec 1995 00:12:00 GMT","details":"$1"}&1={"firstName":"$2","lastName":"$3:foo"}&2="John"&3={"foo":"Doe"} ``` 解析后,上述内容在服务器内存中解析为: ``` {"email":"[email protected]","updated":Date(Mon Dec 04 1995 13:12:00 GMT+1300 (New Zealand Daylight Time)),"details":{"firstName":"John","lastName":"Doe"}} ``` ### "一个明显的安全检查缺失" 关键的是,Flight 允许引用对象的属性。那么如果我们尝试引用一个不在对象本身而在其原型上的属性,会发生什么?果不其然,如果我们向服务器发送一个引用继承属性的 Flight 消息(在这种情况下是 `Number.prototype.toString`),它会成功检索到它并放在我们的攻击者可控对象上。 ``` 0={"foo":"$1:toString"}&1=123 ``` 解析后: ``` 0={"foo":Number.prototype.toString} ``` 这被 Guillermo Rauch(Next.js 的原始作者和 Vercel 的创始人)描述为"一个明显的安全检查缺失"(https://www.linkedin.com/pulse/react2shell-guillermo-rauch-cwl2c)。然而在当时,我实际上并没有想太多。它看起来……过于宽松,但它遵循标准 JavaScript 属性查找语义。React 是经过最严格测试的框架之一,所以它一定是安全的,*对吧*? ## 周二 将 Flight 武器化 ### 最初的目标 通过我的初步研究,我有两个关键想法: 1. 开发者经常忽略验证用户输入。 2. Flight 允许攻击者发送比普通 JSON 复杂得多的对象。 这可能是一个强有力的组合。我不是在寻找 Flight 本身的漏洞(还),而是看看 Flight 是否可以被*滥用*来利用验证不足的 Next.js 应用。我审查过的几乎每个 Next.js 应用都包含这种攻击面,包括许多开源项目,所以我希望将 Flight 武器化,建立一个漏洞利用方法论并获得一些 CVE。 (附带说明:当 React2Shell 被修复时,它也关闭了这些很酷的攻击向量,使这些最初的想法变得毫无意义。) ### 示例 1 - 类型强制转换 这是一个开发者可能实现的非常简单的 React Server Function,但有一个微妙的漏洞: ```typescript async function sayHello(name: string): string { 'use server' return 'Hello, ' + name + '!' } ``` 这错误地假设 `name` 是一串——但这只是一厢情愿的想法。它实际上可能是攻击者可以用 Flight 发送的任何对象。那么,如果我們发送一个自定义对象,其 `toString` 被引用,会发生什么?当尝试将其与 `'Hello',` 连接时,服务器将隐式调用 `toString` 函数!特别容易忽略这个错误,因为 TypeScript 注解 `: string` 提供了类型安全的假象;然而,这实际上在运行时并不强制执行。 ### 示例 2 - 显式函数调用 `toString` 示例简单明了,但似乎最难利用,因为只有一次函数调用且没有可控参数。相比之下,这个示例有更多攻击面。开发者再次假设 `str` 是一串,但攻击者可以在 `replaceAll` 上放置一个恶意函数并控制两个参数。 ```typescript async function replaceStuff(str: string, before: string, after: string): string { 'use server' return str.replaceAll(before, after) } ``` ### 类型安全的假象 *"但是 Lachlan,这些示例中的参数显然有类型!"* 我可以听到你们中的一些人大声说。然而,TypeScript 只执行构建时分析——它无法在运行时验证或强制执行不可信数据的类型。这把双刃剑可以非常令人信服地让用户输入看起来是有效的类型,但实际上什么都没有检查。攻击者仍然可以发送任意对象,即使开发者期望的是特定类型。 ### 漏洞利用 - 游戏规则 Flight 提供了弹药,但实际将其转化为有用的武器很困难。就像一个 CTF 谜题一样…… 假设应用开发者编写了不安全的代码(如前面的示例): - 攻击者指定的函数被调用一次 - 攻击者控制 0 到 N 个参数 - 无论我们指定什么函数,无论我们的参数采取什么形状,都必须来自 Flight 消息。 浏览 React 文档和 Flight 代码表明我们可以使用以下数据类型: 1. 普通对象、数组、字符串 2. JSON 无法表示的常量(`Infinity`、`BigInt`、`NaN` 等) 3. `Date` 4. `Set`、`Map` 和类型化数组,如 `Uint8Array` 5. 一个 Promise,用于将在未来到达的数据块 6. 对 Server Functions 的引用 7. 上述任何属性或方法 最后一点是最重要的,因为这是我们可以开始引用函数的地方。例如,我们可以创建一个 `Array`,然后引用 `Array.prototype.join`。或者,我们可以创建一个 `Date` 来引用 `Date.prototype.getYear`,等等。 对于任何函数,我们可以使用 `.constructor` 访问 `Function` 构造函数,这是动态执行任意 JavaScript 代码的少数几种方式之一。首先,你用要执行的代码调用它,它返回一个函数。当你执行*那个*函数时,你的任意代码就会执行。如果我们能强制执行 `Function("console.log('evil')")()`,我们就赢了。 通过游戏规则,调用一次函数很容易。然而,随后调用结果似乎几乎不可能。 在这一点上,我向不少朋友寻求建议和想法。但特别要感谢 Sylvie Mayer(https://x.com/_sy1vi3),她是我多年的朋友。知道她在类似性质的 CTF 挑战中技术高超,我向她展示了我到目前为止对 Flight 的研究和上述谜题的"规则"。她有关于这个故事的自己的博客文章(https://sylvie.fyi/posts/react2shell/),以及她博客上的一些很棒的文章,比如 Python 沙盒挑战,与此有相当相似的技能组合。 ## 周三 荒谬的循环 我仍然必须围绕这个安排正常的日常工作,但在这一周,我忍不住把每一个醒着的时刻都花在这个新发现的痴迷上。除此之外,多巴胺使我无法入睡。我已经认为自己对 JavaScript 非常了解,但我不断学到更多关于它的特性、Flight 给予我们的访问权限,以及我们如何让它行为不端。 Sylvie 和我在这里花了相当多的时间试图将 Flight 武器化,但每次我们发现看起来有希望的东西,我们尝试并失败使其对实现 RCE 有用。但在每次迭代中,我对这种奇怪的协议以及它给予我们的访问权限变得更加流利。 然而有趣的是,我们两个都不断陷入这种心理循环: 很难回首并个人反思这一点。显然,我有一个巨大的认知盲点。React 在安全方面近乎完美的记录使找到这样的漏洞的想法看起来很荒谬。我上面给出的不安全应用代码示例(将不可信用户输入当作字符串处理)实际上出现在 React 本身内部!这使我几乎不考虑这些情况作为利用的潜在途径。 在这一点上,我们最好的希望是 React/Flight 让我们构建一个恶意函数来执行,然后易受攻击的应用实际上会调用它。 ## 周四 第一次突破 在星期四晚上的某个时候,我发现了导致我开发 React2Shell 的最关键成分之一。当高级框架(如 Next.js)要求 React 解码传入的 Flight 有效载荷时,它调用 `await decodeReply(...)`。它看起来非常合理,但隐藏着一个棘手的秘密…… ### 快速历史课 - 回调和 Promise 服务器端 JavaScript 代码在单个线程中运行,允许 I/O 由引擎在后台线程中用原生代码处理。在 JS 世界中,你传递一个"回调"函数,让引擎在后台操作完成时调用。 ```javascript doTheThing(123, function(result) { doTheNextThing(result.blah, function() { moreStuff(...) }) }) ``` 然而,这变得非常混乱。如果你需要 10 个顺序数据库调用来处理传入的请求,你需要 10 层缩进,导致"回调地狱"。这促使了"Promise"的发明——一种表示未来值承诺的标准格式。使用 Promise,你调用 `.then(...callback...)` 而不是直接传递回调函数。如果你的 `.then` 处理器返回另一个 Promise,你可以继续链接 `.then` 调用。你不需要为每个调用缩进,只需链接另一个 `.then(...)`。 ```javascript doThething(123) .then((result) => doTheNextThing(result.blah)) .then(() => moreStuff()) .then(() => andEvenMoreStuff()) ``` 这是一个很大的改进,但仍然不是很人体工学。但然后,ECMAScript 2017 终于将 `async/await` 引入 JS,允许像同步代码一样编写异步代码: ```javascript let result = await doTheThing(123) await doTheNextThing(result.blah) await moreStuff() await andEvenMoreStuff() ``` 标记为 `async` 的函数在调用时只返回一个 `Promise`,而 `await` 只是对 `Promise` 调用 `.then(...)`,使 `async/await` 与 Promise 完全互操作! 然而……在 Promise 成为 ECMAScript 2015 的标准之前,有一堆社区实现的 Promise 实现,通常具有不同的行为。但它们都有一个共同点:`.then(...)` 约定。 所以,我们称任何遵循这个约定的对象为"thenable"——字面上任何可以 `.then()` 的对象。在底层,`await` 调用 `Promise.resolve`,它宽松地调用 thenable。 ```javascript > await { then: console.log } function (), function () // logged injected resolve, reject callbacks ``` 但还有一个细节:你曾经需要 `await await foo()` 吗?不!如果一个 thenable 解析为*另一个 thenable*,它也会被自动调用。 ```javascript > await { then: resolve1 => resolve1({ then: resolve2 => resolve2(123) }) } 123 ``` ### 滥用 Thenable 那么如果我们将以下内容序列化和发送到 Flight,会发生什么? ```javascript { then: Array.prototype.push } ``` 好吧,`await decodeReply(...)` 将首先运行 Flight 解析器。但是,它然后看到它得到了另一个 thenable,并调用我们的攻击者提供的函数!请求将挂起,因为我们的假 Promise 永远不解析。但如果我们检查内存中的内容,我们发现运行时确实在我们的精心设计的对象上调用了 `.then(resolve, reject)`: ```javascript { then: Array.prototype.push, 0: [Function (anonymous)], 1: [Function (anonymous)], length: 2 } ``` 这是第一个感觉不像是 React 故意设计的行为。到目前为止,Flight 提供的所有东西感觉……很过度,但从我的外部角度来看,它看起来就像你给定它的设计。 再进一步,如果我们精心制作我们的 thenable 有效载荷以解析为另一个 thenable,我们可以链接无限次函数调用。然而在当时,我没有充分…

相似文章

React2Shell 的故事以及 Next.js 的后续发展

Lobsters Hottest

本文详细介绍了 CVE-2025-5518(React2Shell)的发现与披露过程。这是一个存在于 React Server Components 中的严重远程代码执行漏洞,研究人员通过绕过 Flight 协议的验证机制来访问对象原型。

真的有人喜欢 React 吗?

Hacker News Top

关于 React 的批评性博客文章合集,涵盖性能问题、一个严重安全漏洞(CVE-2025-55182,CVSS 10.0)以及更广泛的生态系统问题。

AMD不愿修复的远程代码执行漏洞

Hacker News Top

一名研究人员发现AMD的AutoUpdate软件存在远程代码执行漏洞,原因在于不安全的HTTP下载链接和缺乏证书验证。AMD最初以超出范围为由不予理会,但在公众关注后同意发布CVE并修复。