Lessons from React2Shell
Source: Dev.to
On December 3rd 2025, React disclosed CVE‑2025‑55182, a critical remote code execution vulnerability with a CVSS score of 10.0—the maximum possible severity. Within hours, attackers were exploiting it in the wild. Nearly a million servers running React 19 and Next.js were vulnerable to unauthenticated remote code execution. For a framework that had maintained a remarkably clean security record over 13 years—only a single XSS vulnerability (CVSS 6.1) in 2018—this represented a catastrophic failure.
The Vulnerability
The exploit exists in React’s “Flight” protocol, a custom serialization format introduced with React Server Components. Flight handles the transfer of data and execution context between client and server. The vulnerability allowed attackers to craft malicious payloads that, when deserialized by the server, could execute arbitrary code. The attack required no authentication—just network access to send a crafted HTTP request to any Server Components endpoint.
The technical root cause was unsafe deserialization of untrusted client data. The server accepted serialized objects from clients, deserialized them, and executed code based on their contents, including accessing properties like .then and .constructor that exposed JavaScript’s code‑execution primitives. React’s defenses relied on the assumption that the serialization format itself would prevent malicious inputs, rather than treating all client data as untrusted by default.
What Are React Server Components?
React Server Components (RSC) represent a fundamental shift in React’s architecture. Traditionally, React was a client‑side library that ran in the browser, rendering user interfaces and talking to backend APIs via standard REST or GraphQL endpoints. The backend could be written in any language: Python, Go, Ruby, Java, etc.
Server Components change this model. They allow React components to execute on the server, access databases directly, and serialize their results—including promises and complex state—to the client using the Flight protocol. Functions marked with 'use server' become server‑side endpoints automatically, with no explicit API routes required. The framework handles routing these “Server Actions” and serializing the data flow between client and server.
The pitch is seductive: write your frontend and backend in the same files, using the same language, with “seamless” data flow between them. No API boilerplate, no context switching, just components that “magically” know whether to run on client or server.
Violation of Security Principles
React abandoned decades of hard‑won security wisdom. The fundamental principle of secure systems is simple: never trust client input. Every mature framework and language ecosystem has learned this lesson through painful experience:
- Java serialization vulnerabilities led to remote code execution in countless applications, eventually prompting deprecation warnings and guidance to avoid deserialization of untrusted data.
- PHP’s
unserialize()became the attack vector for thousands of WordPress compromises; the community now treats deserialization of user input as an anti‑pattern. - Python’s
pickledocumentation explicitly warns: “The pickle module is not secure. Only unpickle data you trust.” - Ruby’s
Marshalcarries the same warnings and history of vulnerabilities.
React built a custom serialization protocol that deserializes client data into server execution contexts. The Flight protocol needed to be “smarter” than JSON—capable of serializing promises, closures, and complex object graphs. This added complexity made it inherently dangerous.
The vulnerability was not a stray implementation bug; it was the predictable consequence of violating a fundamental security principle: complex deserialization of untrusted data leads to remote code execution. If you can’t do it perfectly, don’t do it at all.
Traditional REST APIs avoid this class of vulnerabilities by using JSON, a deliberately limited data format that carries no execution context, no code, no object methods. JSON is “dumb” in exactly the right way: it’s just data structures. The server receives JSON, validates it against expected schemas, and explicitly routes it to the appropriate handler. There’s no deserialization of execution contexts, no automatic invocation of client‑specified code paths, and no blurred boundaries between data and code.
Tight Coupling: The API That Isn’t
React Server Components don’t just introduce security risks; they eliminate architectural flexibility. When you mark a function with 'use server', you haven’t created an API—you’ve created a React‑specific endpoint that can only be called by React clients using the Flight protocol.
Traditional REST API Example (Python)
@app.post('/api/posts')
def create_post(data):
return db.posts.create(data)
This endpoint can be called by:
- Your React frontend
- Your mobile app (iOS/Android)
- Your CLI tool
- Partner integrations
- Third‑party developers
- Any HTTP client in any language
- Testing tools like
curlor Postman
It can be documented with OpenAPI/Swagger, monitored with standard HTTP tooling, and secured with conventional WAF rules.
Server Action Example (JavaScript)
'use server'
async function createPost(data) {
return await db.posts.create(data);
}
This can be called only by your React frontend. It uses a proprietary protocol (Flight) that only React understands. It can’t be documented in a language‑agnostic way, and standard HTTP monitoring tools can’t parse the payloads. Security tools can’t inspect the traffic. If you need a mobile app, you’ll still have to build a separate REST API.
You haven’t eliminated API boilerplate; you’ve hidden it behind framework magic while simultaneously limiting who can use it. When your application inevitably needs to support multiple client types—web, mobile, CLI—you’ll end up maintaining two parallel systems: Server Actions for your React web app and a proper REST API for everything else. The “convenience” of Server Components becomes technical debt the moment you need to integrate with anything outside the React ecosystem.
The reusability problem extends beyond multiple clients. Modern applications often need to expose webhooks, integrate with partner APIs, or provide data to analytics platforms—none of which can consume React Server Actions. You’re forced back to building traditional API endpoints, making Server Actions redundant: a solution in search of a problem that creates more problems.
JavaScript Lock‑In: Losing the Right Tool for the Job
Perhaps the most insidious aspect of React Server Components is the way they eliminate architectural choice. For 13 years, React worked with any backend. Your API server could be written in Python for data science, Go for high‑performance services, Rust for systems programming, Java for enterprise integration, or Ruby for rapid development—the choice was yours, based on your team’s expertise and your application’s requirements.
Server Components change this equation fundamentally. To use them, your server must be JavaScript—specifically, Node.js or a compatible runtime. The Flight protocol, the Server Actions routing, and the entire Server Component ecosystem are tightly coupled to the JavaScript runtime, forcing teams to adopt a technology stack that may not be optimal for their domain. This lock‑in reduces flexibility, increases operational complexity, and can lead to sub‑optimal performance or higher maintenance costs.