Skip to content
HN On Hacker News ↗

The React2Shell Story

▲ 225 points 47 comments by mufeedvh 2w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

4 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 5 of 5
SEGMENTS · AI 0 of 5
WORD COUNT 1,708
PEAK AI % 7% · §3
Analyzed
May 9
backend: pangram/v3.3
Segments scanned
5 windows
avg 342 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,708 words · 5 segments analyzed

Human AI-generated
§1 Human · 3%

On November 30th 2025, I reported a critical remote code execution vulnerability ("React2Shell") to Meta. On December 3rd, Meta released a fix and public advisory (CVE-2025-55182), urging developers to immediately update.Funnily enough, I didn't set out to find a vulnerability in React. I just wanted to understand a protocol so I could be better at hacking modern web applications. But instead, I fell down a rabbit hole to a critical vulnerability that affected millions of websites.I also recommend reading Sylvie's blog post on React2Shell, and the shenanigans following it. Dates in this post are displayed in NZDT (GMT +13). This contrasts to Sylvie's post (GMT -7) and Meta's (GMT -8) Monday - Taking FlightAs a professional hacker, Monday 24 November 2025 started as a normal work day: finishing off reports, starting new projects, etc. But that afternoon, fueled by curiosity and frustration, I felt a switch flip in my brain, and I dived head-first into a rabbit hole with no turning back.Some BackgroundIn recent years, I've pentested plenty of web apps built on Next.js - a very popular framework based on React. Next.js makes use of React Server Components (RSC) to efficiently render content on the server and send it to the user's browser, as well as React Server Functions (formerly Server Actions) to let user interactions seamlessly invoke server-side JavaScript code.Many ridiculed Server Actions when they were introduced (you may remember this picture doing the rounds?), but it caught on as it's genuinely quite a cool feature. In one codebase, developers can write server-side code and call it from client-side code.To facilitate these features, your browser and the server need a fancy way to send messages back and forth, which existing technologies weren't quite suitable for. So, the React team had to build something new.

§2 Human · 4%

Anybody who has pentested a web app that uses Server Functions should be familiar with this slightly odd request format:0=[{ "a": "$$undefined", "b": "$1:foo:bar" }]&1=... Collectively as an industry, I think we've all thought "Meh, it just looks like JSON with some bells and whistles added", and proceeded to test this like a traditional app, despite the fact there's clearly more attack surface here. I was certainly guilty of this.Where the Switch FlippedOn this Monday, I developed a relentless urge to learn about this format. I needed to understand it. Those who know me will attest that I don't do things by halves, and will gladly go to the ends of the earth to tear a problem apart.Most web application hacking is just throwing stuff at applications that the developers didn't anticipate. If this protocol gave me more unique things to throw at web apps, I simply had to know; I could perhaps even publish a methodology for it!Wait, but what is it?So I started by looking at the docume-- oh... there is no specification for the protocol, and the documentation that does exist is scant on details.What is this protocol even called? It took me some digging to learn its name is "Flight". This is easy to find now, but before the disclosure of React2Shell, it was surprisingly difficult to even find the name, let alone the format of the protocol.The best information I could find was a few threads on X discussing RSC.I was already hooked on trying to understand Flight, but seeing "no docs, only code" poured fuel on the fire of my motivation. My fate was now sealed; I would not rest until I was a Flight expert.I hardly got a wink of sleep, but by the next morning, I had a good understanding of the protocol's fundamentals, though this was just the beginning.Flight 101Next.js allows developers to magically pass complex JavaScript objects between client-side and server-side code, but this includes objects that cannot be represented by simple JSON. Flight solves that problem, adding support for more complicated data types (such as Date, BigInt, and Map), references (including circular references), and Promises (data that arrives asynchronously), to name a few of its features.It's still fundamentally based on JSON, but Flight messages are broken into "chunks".

§3 Human · 7%

Each chunk is typically sent as a form element, and can arrive asynchronously, potentially out-of-order. The special $ syntax denotes a Flight type. Seen below, $D indicates a date, $x is a reference to another chunk, and $x:y allows property selection.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" } Once parsed, the above resolves to the following in the server's memory:{ "email": "[email protected]", "updated": Date(Mon Dec 04 1995 13:12:00 GMT+1300 (New Zealand Daylight Time)), "details": { "firstName": "John", "lastName": "Doe" } } A "Glaring Omission of a Safety Check"Crucially, Flight allows referencing an object's properties. So what happens if we try to reference a property that isn't directly on an object, but rather on its prototype?Lo and behold, if we send the server a Flight message that references an inherited property (in this case, Number.prototype.toString), it successfully retrieves it and places it on our attacker-controllable object.0 = { "foo": "$1:toString" } &1 = 123 0 = { "foo": Number.prototype.toString } This was described by Guillermo Rauch (the original author of Next.js and founder of Vercel) as "a glaring omission of a safety check". However, at the time, I honestly didn't think too much of this. It seemed... excessively lenient, but it abided by standard JavaScript property lookup semantics. React is one of the most battle-tested frameworks out there, so it must be fine, right?Tuesday - Weaponising FlightThe Initial GoalWith my initial research, I had two key things in mind: Developers frequently neglect to validate user inputs. Flight can let attackers send significantly more complex objects than plain JSON.

§4 Human · 7%

This could be a potent combination. I wasn't looking for a vulnerability in Flight itself (yet), but rather to see if Flight could be abused to exploit Next.js apps with insufficient validation.Almost every Next.js application I've reviewed has contained such attack surface, including many open-source projects, so I hoped to weaponise Flight to build an exploit methodology and earn some CVEs.(Side note: When React2Shell was fixed, it also closed off these cool attack vectors, making these initial ideas moot.)Example 1 - Type CoercionHere is a very simple React Server Function a developer might implement, but with a subtle vulnerability:async function sayHello(name: string): string { 'use server'

return 'Hello, ' + name + '!' } This incorrectly assumes name is a string - but that's just wishful thinking. It may actually be any object that a malicious client can send with Flight. So what happens if we send a custom object with a function referenced on toString? When attempting to concatenate it with 'Hello', the server will implicitly call the toString function!It's especially easy to gloss over this mistake, as the TypeScript annotation : string provides an illusion of type safety; however, this is not actually enforced at runtime.Example 2 - Explicit Function CallsThe toString example is simple to identify, but seemed the hardest to exploit, as it's only one function call with no controllable arguments.On the contrary, this example has significantly more attack surface. The developer has once again assumed str is a string, but an attacker could place a malicious function on replaceAll and control two arguments to it.async function replaceStuff(str: string, before: string, after: string): string { 'use server'

return str.replaceAll(before, after) } The Illusion of Type Safety"But Lachlan, the parameters in those examples clearly have types?!", I can hear some of you exclaim.However, TypeScript only performs build-time analysis - it can't validate or enforce the type of untrusted data at runtime. This double-edged sword can make it very convincing that the user input is a valid type, but nothing actually checks this. An attacker can still send an arbitrary object, even when the developer expects something specific.Exploitation - The Rules of the GameFlight provides the ammunition, but actually turning it into a useful weapon was difficult.

§5 Human · 2%

To present this like a CTF puzzle...Assuming an application developer has written insecure code (like the prior examples): An attacker-specified function is called once The attacker controls 0 to N arguments Whatever function we specify, and whatever shape our arguments take, must come from Flight messages. Skimming the React docs and Flight code indicates we can use the following data types: Plain objects, arrays, strings Constants that JSON can't represent (Infinity, BigInt, NaN, etc.) Date Set, Map, and typed arrays, such as Uint8Array A Promise for a chunk that will arrive in the future References to Server Functions Any property or method on any of the above The final point is the most important, as this is where we can start referencing functions. For example, we can make an Array, then reference Array.prototype.join. Or, we can make a Date to reference Date.prototype.getYear, and so on.With any function, we can use .constructor to get access to the Function constructor, which is one of the few ways to dynamically execute arbitrary code in JavaScript. First, you call it with the code to execute, and it returns a function. When you execute that function, your arbitrary code executes.If we could coerce Function("console.log('evil')")(), we'd win. Through the rules of the game, calling a function once is easy. However, subsequently calling the result seemed near-impossible.At this point, I asked for advice and ideas from quite a few friends. But in particular, I'd like to thank Sylvie Mayer who I've been friends with for many years. Knowing that she's highly-skilled at CTF challenges of a similar nature to this, I approached her with my research so far into Flight and the aforementioned puzzle 'rules'. She has her own blog post on this saga, and also some great write-ups on her blog, such as Python jail challenges that have quite a similar skill set to this.Wednesday - The Ridiculous CycleI still had to fit normal work days around this, but throughout this week, I couldn't help but spend every other waking moment on this newfound obsession. On top of that, the dopamine made sleep out of the question. I already considered myself very knowledgeable about JavaScript, but I kept learning more and more about its quirks, what Flight gave us access to, and how we could make it misbehave.