The React2Shell Story and What Happened Next.js

The React2Shell Story and What Happened Next.js

React2Shell 的故事以及 Next.js 发生了什么

On December 3rd 2025, Meta disclosed CVE-2025-55182 which we dubbed react2shell, an unauthenticated RCE in React Server Components. In short, the Flight protocol failed to properly validate types, allowing the construction of arbitrary chunks and access to object prototypes (and therefore the functions on them). Several very well-written technical breakdowns of the vulnerable Flight code have already been made, most notably by Lachlan Davidson, my research partner in this. I’ve been asked by a handful of friends when I’m going to post my own technical writeup, and while I feel as though writing one would be rather redundant at this point, I do think the story of how exactly we stumbled across this and what happened afterwards is worth telling. Before starting, I’d like to note that Lachlan and I were in very different time zones, I was in GMT-7 to his GMT+13, so our dates and times will be offset from each other by 20 hours.

2025 年 12 月 3 日,Meta 公布了 CVE-2025-55182,我们将其命名为 react2shell,这是一个存在于 React Server Components 中的未经身份验证的远程代码执行(RCE)漏洞。简而言之,Flight 协议未能正确验证类型,从而允许构建任意数据块并访问对象原型(进而访问原型上的函数)。目前已经有几篇关于该漏洞 Flight 代码的非常出色的技术分析文章,其中最著名的是我的研究伙伴 Lachlan Davidson 所写的。不少朋友问我什么时候发布自己的技术总结,虽然我觉得现在再写一篇有些多余,但我认为讲述我们究竟是如何偶然发现这个漏洞以及后续发生的事情是非常有意义的。在开始之前,我想说明一下,Lachlan 和我处于完全不同的时区,我处于 GMT-7,而他在 GMT+13,因此我们的日期和时间会有 20 小时的时差。

Monday - The Saga Begins

周一 - 传奇的开始

This all began for me in late November when Lachlan, a friend I met through robotics back in 2019, noticed something curious and asked about it in a small group chat we’re both in. This problem felt like it could be a fun javascript jail challenge, so I took interest and spent the evening hunting for anything that could be of interest. Initially, we only had access to the primitive types made available by Flight, those being String, Number, Array, Map, Date, BigInt, Object, and Function. The gadgets we had access to at the time only allowed us to call a single function we had the ability to reference with a single argument. With this, it was pretty easy to do something like Object.constructor.constructor(“console.log(‘meow’)”), to build an arbitrary function object, but with no ability to access the result, the function remained un-called and useless. While Lachlan originally noticed this behavior in the way the CMS platforms used React, it quickly became obvious that the same sort of primitives existed in React itself. I spent the rest of the evening exploring the problem from a Node REPL to get a better feel for what we were dealing with. At this point, none of us really thought this would get anywhere. There were many comments, between Lachlan, myself, and the others in the group chat, mostly along the lines of “lol wouldn’t it be crazy if we found an RCE in React”. It was something within the realm of possibility, but we figured that if this was actually vulnerable, someone would have probably found it already. Despite this, both Lachlan and I agreed that the behavior in React was clearly quite poorly considered, even if it wasn’t directly vulnerable, so we kept looking anyways. I kept poking at this problem with Lachlan over the next few days. We made slow and incremental progress, but didn’t make any real breakthroughs.

这一切始于 11 月下旬,当时我 2019 年在机器人技术活动中结识的朋友 Lachlan 注意到了一些奇怪的事情,并在我们共同的一个小群聊中询问。这个问题感觉像是一个有趣的 JavaScript 越狱挑战,所以我产生了兴趣,花了一晚上时间寻找任何可能感兴趣的东西。最初,我们只能访问 Flight 提供的原始类型,即 String、Number、Array、Map、Date、BigInt、Object 和 Function。当时我们能利用的“小工具”(gadgets)只允许我们调用一个可以通过单个参数引用的函数。有了这个,很容易写出类似 Object.constructor.constructor("console.log('meow')") 这样的代码来构建一个任意函数对象,但由于无法获取结果,该函数始终未被调用且毫无用处。虽然 Lachlan 最初是在 CMS 平台使用 React 的方式中注意到这种行为的,但很快就很明显,React 本身也存在同样的原始类型。我那天晚上剩下的时间都在 Node REPL 中探索这个问题,以便更好地了解我们正在处理什么。当时,我们谁也没想到这会有什么结果。Lachlan、我和群里的其他人开了许多玩笑,大多是说“哈哈,如果我们能在 React 中发现 RCE 那该多疯狂”。这虽然在可能性范围内,但我们认为如果它真的有漏洞,别人可能早就发现了。尽管如此,Lachlan 和我都认为 React 中的这种行为显然考虑不周,即使它没有直接的漏洞,所以我们还是继续研究。接下来的几天里,我一直和 Lachlan 一起钻研这个问题。我们进展缓慢且零碎,但没有取得任何真正的突破。

Thursday - This is Maybe Getting Serious

周四 - 事情可能变得严重了

At a certain point, we realized that with a non-zero chance of discovering something truly catastrophic, we should probably stop talking about any further work in a place with more than just us, so we moved to DMs. We had both become convinced that there was certainly some way to get RCE here; it was just a matter of figuring out how. From here, I decided to join Lachlan in looking at NextJS’s implementation specifically, because of several widely-used React-based frameworks to pick from, it seemed to be the most useful to have a potential exploit working against. I used the default Next project as a base to work off of. One of the things Lachlan discovered and shared with me was the ability to access certain imports and bind arguments to them. The issue with this, however, was that Webpack seemed to give us very little we could actually import that might be of use. I spent a few hours going through the default server configuration, looking for imports that could possibly contain something useful, to little success. By around 4:30 in the morning, I was on the verge of passing out at my desk, so I called it a night. I woke up the next afternoon with nothing else on my schedule, aside from the Thanksgiving dinner party I was supposed to spend the entire evening at. As should be entirely predictable to anyone who knows me, I spent the vast majority of that dinner party on my phone reading Node source and documentation, specifically looking at the module module. After getting home from dinner, I immediately went back to my desk and resumed my search. It was at this point trivial to execute JS from disk, if only it were possible to get it there. I focused my energy on attempting to force some bespoke file upload to work, while Lachlan explored other avenues. I ended up going to bed around 2:30am that night. At around 5am my time, Lachlan messaged that he’d done it.

在某个时刻,我们意识到发现真正灾难性漏洞的可能性并非为零,我们可能应该停止在多人可见的地方讨论后续工作,于是我们转到了私信。我们都确信这里肯定有某种方法可以实现 RCE;只是需要找出具体方法。从这里开始,我决定加入 Lachlan,专门研究 Next.js 的实现,因为在几个广泛使用的基于 React 的框架中,针对它开发潜在的漏洞利用似乎最有价值。我使用默认的 Next 项目作为基础进行研究。Lachlan 发现并与我分享的一件事是能够访问某些导入项并为其绑定参数。然而,问题在于 Webpack 似乎几乎没有提供我们可以导入且有用的东西。我花了几个小时检查默认的服务器配置,寻找可能包含有用内容的导入项,但收效甚微。凌晨 4:30 左右,我快要在桌子上睡着了,于是我结束了当晚的工作。第二天下午醒来时,除了我必须参加的感恩节晚宴外,我没有任何其他安排。正如了解我的人所预料的那样,我在那场晚宴的大部分时间里都在手机上阅读 Node 的源代码和文档,特别是关于 module 模块的部分。晚宴回家后,我立即回到桌前继续搜索。此时,从磁盘执行 JS 已经变得轻而易举,前提是能把它放进去。我集中精力尝试强制实现某种定制的文件上传,而 Lachlan 则探索了其他途径。那天晚上我大约凌晨 2:30 上床睡觉。在我这边大约凌晨 5 点时,Lachlan 发来消息说他成功了。

Friday - Panic

周五 - 恐慌

At this point, I’d spent around 36 hours actively working on this, and by his estimation, Lachlan had spent well over a hundred. Despite how it might have seemed, the hard part was far from over. Neither of us being stupid, we both immediately realized that we were sitting on what was effectively a nuclear bomb. Despite a fairly high level of trust and having been friends for over half a decade, both of us began moving quite defensively, unsure exactly what the other would do. I didn’t have the full RCE PoC at this point, but I was confident I could get there rather quickly. We decided the best thing for both of us to do was to calm down get some sleep. After both of us came back to our senses, we decided it wise to get a signed, on-paper agreement between the two of us to ease the anxiety of anticipating the other’s plans. The general terms were that Lachlan would disclose the vulnerability to Meta, that we’d both share any current or future relevant PoCs with each other, and that neither of us would disclose any information that would help anyone arrive at the PoC with anyone else without mutual agreement. At this point…

此时,我已经在研究这个问题上投入了大约 36 个小时,据 Lachlan 估计,他投入的时间远超一百个小时。尽管看起来已经成功,但最困难的部分远未结束。我们都不傻,我们立刻意识到我们手里掌握着一颗实际上是“核弹”的东西。尽管我们有相当高的信任度,并且已经做了五年多的朋友,但我们两人都开始采取防御姿态,不确定对方到底会做什么。当时我还没有完整的 RCE PoC(概念验证),但我有信心很快就能实现。我们决定,对我们两人来说最好的做法是冷静下来,睡一觉。在我们都恢复理智后,我们认为签署一份书面协议是明智之举,以缓解对对方计划的焦虑。基本条款是:Lachlan 将向 Meta 披露该漏洞,我们双方将分享任何当前或未来相关的 PoC,并且在未经双方同意的情况下,我们任何一方都不会向其他人披露任何有助于他人推导出该 PoC 的信息。此时……