The Next.js Turducken
A reflection on React2Shell, invisible boundaries, and why the browser-server separation exists
I've been building web applications for a long time. I started my career writing PHP, and I'll be honest: I hated it. I eventually found my way to Rails, fell in love with Ruby's expressiveness, took a deep dive into Elixir and Phoenix, dabbled in Laravel (PHP redeemed, somewhat), and ultimately landed on TypeScript as my language of choice. These days I do true full-stack TypeScript: server-side, client-side, and even infrastructure via Pulumi.
I've written a lot of React. I don't love it. I don't hate it. It's a tool that solves a problem.
But that problem has been stretched far beyond its original scope. And last week, we saw the consequences.
The CVE That Proved the Point
On December 3rd, 2025, security researchers disclosed CVE-2025-55182, dubbed "React2Shell." It's a CVSS 10.0 vulnerability. The maximum severity score. Unauthenticated remote code execution in React Server Components.
The timeline was brutal:
- November 29th: Vulnerability reported to Meta
- December 3rd: Public disclosure
- December 4th: Public proof-of-concept exploits available
- December 5th: Active exploitation observed in the wild
Within hours of disclosure, state-sponsored threat actors were exploiting it. Amazon's threat intelligence team observed China-nexus groups including Earth Lamia and Jackpot Panda actively scanning for vulnerable applications. Palo Alto Networks identified activity from CL-STA-1015, an initial access broker with suspected ties to the PRC's Ministry of State Security, deploying SNOWLIGHT and VShell trojans.
The scale? Palo Alto identified over 968,000 React and Next.js instances in their telemetry. Wiz Research reported that 39% of cloud environments contained vulnerable instances.
Then it got worse. On December 11th, two additional vulnerabilities were disclosed: CVE-2025-55183 (source code exposure) and CVE-2025-55184 (denial of service). The initial fix for the DoS vulnerability was incomplete, requiring yet another patch.
This wasn't a fluke. This was inevitable.
The Turducken Architecture
For those unfamiliar with the culinary abomination: a turducken is a chicken stuffed inside a duck stuffed inside a turkey. It's impressive in a grotesque way, but nobody would call it elegant.
Next.js is the turducken of web frameworks.
It started as a server-side rendering solution for React. A reasonable tool for a specific problem: React apps needed better SEO and faster initial page loads. SSR solved that.
Then they stuffed API routes inside it. Now your frontend framework is also your backend.
Then Server Components. Now some of your React components run on the server, some on the client, and you need to reason about which is which.
Then Server Actions. Now you can call server functions directly from your components with a "use server" directive. The boundary between client and server becomes a single line of code.
Then a custom caching layer that nobody fully understands. Then a new App Router with different semantics than the Pages Router. Then edge functions. Then partial prerendering.
Layer after layer after layer, each one adding complexity and new vectors for exploitation. Each one blurring the line between what runs in the browser and what runs on the server.
The Boundary Exists for a Reason
Here's what the React Server Components evangelists don't want to talk about: the browser-server boundary isn't an inconvenience to abstract away. It's a security perimeter.
We've spent decades hardening this boundary. HTTP, REST, GraphQL: these aren't just data transfer protocols. They're chokepoints where we validate, sanitize, authenticate, and authorize. Every request crossing that boundary gets scrutinized because we operate under a fundamental assumption: the client is hostile.
This isn't paranoia. It's security architecture 101. The browser is an untrusted execution environment. Users can modify requests. They can replay them. They can craft malicious payloads. The server's job is to assume every input is potentially malicious and handle it accordingly.
Traditional MVC frameworks have internalized this. When you write a Rails controller action, you're explicitly handling a request that crossed the trust boundary. You validate params. You check authorization. You sanitize input before it touches your database. The friction of writing that endpoint isn't a bug. It's a feature. It forces you to think about what crosses the wire.
React Server Components try to make this boundary invisible. "Just call a function!" they say. "It works the same on client and server!"
But under the hood, there's a serialization layer called the "Flight" protocol, shuttling data between browser and server. That magical transport layer? That's exactly where the RCE vulnerability lived. Unsafe deserialization of untrusted input. One of the oldest vulnerability classes in the book.
The Invisible Boundary Is the Problem
Let me be clear about what I'm not saying: sharing code between frontend and backend is fine. Sharing types, validation schemas, utility functions — that's just good engineering. TypeScript across the stack is genuinely useful.
The problem isn't code sharing. The problem is making the network boundary invisible.
When you call a Server Action, it looks like a function call. But it's not. It's an HTTP request that crosses a trust boundary, gets serialized by a protocol you don't control, and executes on a server that should treat that input as hostile. The syntax hides all of this.
When your code "seamlessly" invokes server logic from the client, you create:
Unclear trust boundaries. Which code runs where? With Server Components, a component can render on the server during initial load and on the client during navigation. A Server Action can be called from client code but executes on the server. The mental model required to reason about trust boundaries becomes extraordinarily complex.
Implicit serialization you don't control. When you call a Server Action, your arguments get serialized, sent over the network, and deserialized on the server. This happens automatically. The serialization format (Flight) was invented recently and hadn't received the security scrutiny that JSON, Protocol Buffers, or other mature formats have endured.
Attack surface hidden behind abstractions. When you write an explicit API endpoint, you can see exactly what data you're accepting and how you're processing it. When the framework magically handles the transport, you lose visibility into a critical part of your attack surface.
Deserialization of untrusted input. This is the exact vulnerability class that caused React2Shell. It's also the class that gave us countless Java RCE vulnerabilities and Python pickle exploits. Deserializing untrusted data is inherently dangerous, and invisible boundaries make it easy to forget you're doing it.
The explicit separation between frontend and backend isn't a limitation to overcome. It's a security control. You can share code across the boundary all you want — but the boundary itself needs to be visible, explicit, and treated with respect.
What We Gave Up
I'll take Laravel over Next.js any day. Not because PHP is a better language than TypeScript (it isn't, in my opinion) but because of everything I don't have to think about.
In Laravel, I have:
- A mature ORM with years of security hardening
- Built-in CSRF protection that just works
- Authentication scaffolding that's been battle-tested
- Job queues and background workers out of the box
- Database migrations with proper rollback support
- Logging and observability patterns established by the community
- A clear request lifecycle I can reason about
In Next.js with App Router, I have:
- A caching system that the Next.js team has rewritten twice and developers still complain about
- Server Actions that may or may not be secure depending on how I use them
- No built-in job queue (roll your own)
- No background workers (figure it out)
- Authentication that requires third-party libraries or services
- A mental model where I need to track which code runs where
- A brand new serialization protocol that just had a CVSS 10.0 vulnerability
Rails, Django, Phoenix, Go, Rust. These have years of collective security hardening. And yes, Next.js has years too. So what's the difference?
It's not about who uses what. It's about how these tools make you think.
Stripe processes billions in transactions on Rails. GitHub stores the world's open-source code on Rails. Shopify handles payment processing at massive scale on Rails. These aren't small startups. They're security-critical infrastructure. But the reason they're secure isn't because they chose Rails. It's because Rails forces you to reason about requests explicitly. You write a controller. You validate params. You handle the response. The framework doesn't hide dangerous operations behind magic.
Django, Phoenix, Go, Rust: same principle. They make you write security code rather than abstracting it away. They've undergone years of adversarial scrutiny. The sharp edges have been sanded down. No magic serialization protocols invented last year.
The Evolution That Went Too Far
I want to be clear: I'm not anti-React. React as a client-side rendering library is fine. It solved a real problem: building complex, interactive UIs with a component model and predictable state management.
React for a single-page application? Great.
Next.js for a static site or simple SSR? No problem.
But the trajectory has gone off the rails:
- Client-side rendering (CSR): React's original purpose. Components that manage UI state in the browser. Clear, bounded, sensible.
- Server-side rendering (SSR): Render the initial HTML on the server for SEO and performance. The server renders HTML, ships it to the client, React hydrates and takes over. No controversy here.
- Static site generation (SSG): Pre-render pages at build time. Great for content sites. A legitimate use case.
And here's the thing: for the right use case, Next.js is genuinely excellent. Look at the Node.js website itself. It's built with Next.js, and it's the perfect fit. Documentation pages. Blog posts. Static content with occasional interactivity. No sensitive user data. No complex authentication flows. No business logic that needs protection. The risk profile is essentially zero, and they get the full benefit of the framework: great developer experience, easy deployment, solid performance. That's the sweet spot.
The problem starts when you take a tool optimized for nodejs.org and try to build a banking application with it.
- The turducken era: Server Components, Server Actions, streaming, partial prerendering, the Flight protocol, edge functions, middleware that runs in... where exactly? The server? The edge? It depends?
It isn't any single feature. It's the relentless accumulation of complexity in service of developer experience, without corresponding investment in security architecture.
When you try to make a client-side framework do everything (static sites, dynamic apps, real-time features, server-side logic, background jobs) you end up with something that does everything poorly and has an attack surface the size of Texas.
The LLM Training Problem
There's another dimension to this that doesn't get discussed enough: LLMs are disproportionately trained on Next.js content.
When developers ask AI assistants for help building web applications, they overwhelmingly get Next.js recommendations. This isn't because Next.js is the best tool for every job. It's because Next.js dominates the training data. The Vercel marketing machine, the Twitter influencer pipeline, the YouTube tutorial ecosystem: it all feeds into training corpora that then reinforce the bias.
This creates a dangerous feedback loop:
- Developers ask LLMs for help
- LLMs recommend Next.js because that's what the training data emphasizes
- More Next.js content gets created
- Future training data becomes even more Next.js-heavy
- Goto 1
The result is an entire generation of developers who think "modern web development" means Next.js, who don't know that Rails can ship a feature in half the time, who've never experienced the simplicity of a Go HTTP server, who think you need React for a blog.
LLM providers should consider training models on architectural diversity. Maybe even specialty models tuned for specific paradigms: MVC, SPA, CQRS, whatever. The current monoculture is bad for the industry and bad for security.
Boring Stacks Win
The best architecture for most applications in 2025 is boring. Deliberately, strategically boring.
A clear, explicit boundary between frontend and backend. A backend that treats every request as potentially hostile. A frontend that knows it's a frontend.
Mature frameworks that have earned trust. Rails. Laravel. Django. Phoenix. These aren't sexy, but they've survived decades of adversarial scrutiny. The common vulnerabilities have been found and fixed. The sharp edges have been sanded down.
Simple backend languages when appropriate. A Go HTTP server is easy to reason about, easy to audit, and fast. A Rust backend gives you memory safety guarantees that prevent entire classes of vulnerabilities. These tools don't have the developer experience polish of Next.js, but they have something more important: predictability.
HTMX for interactivity when you don't need an SPA. Most applications don't need a full client-side rendering framework. Server-rendered HTML with sprinkles of interactivity covers 80% of use cases with 20% of the complexity.
React or Svelte when you actually need a rich client. If you're building a complex, interactive application (a design tool, a real-time collaboration app, a data visualization dashboard) client-side frameworks make sense. Use them for what they're good at. Don't let them metastasize into your backend.
Explicit API contracts. Whether it's REST, GraphQL, or tRPC, define the boundary between client and server explicitly. Validate inputs. Handle errors. Make the security perimeter visible and auditable.
The Takeaway
React2Shell isn't an isolated incident. It's what happens when you prioritize developer experience over security fundamentals.
The vulnerability existed because the Flight protocol was new and under-scrutinized, because the serialization boundary was implicit rather than explicit, because the complexity made it hard to reason about trust, and because the push to make server calls "seamless" hid dangerous operations behind convenient APIs.
This will happen again. Not because the React or Vercel teams are incompetent, but because the architecture creates conditions where these vulnerabilities are likely. Every layer of abstraction is a potential hiding place for bugs. Every "seamless" boundary is a trust boundary someone forgot to harden.
I'm not saying never use Next.js. For static sites and simple applications, it's fine. The Pages Router is reasonably well-understood. But if you're building something serious, something that handles sensitive data, something you'll maintain for years, think hard about whether you need the complexity.
There are better options. Laravel. Rails. Django. Phoenix. A Go or Rust backend with a React frontend connected by a well-defined API. Or skip the SPA entirely and reach for HTMX.
The browser and server are separated for your protection. Stop trying to merge them.
Props to Rails, Laravel, Django, Phoenix, HTMX, Go, Hapi, Fastify, NestJS, and everyone who keeps to the founding principles that make up the web. The fundamentals matter.
Sources
- React Security Advisory: CVE-2025-55182 — React Team
- Next.js Security Advisory: CVE-2025-66478 — Next.js Team
- Next.js Security Update: December 11, 2025 — Next.js Team (CVE-2025-55183, CVE-2025-55184)
- China-nexus cyber threat groups rapidly exploit React2Shell — AWS Security Blog
- Exploitation of Critical Vulnerability in React Server Components — Palo Alto Networks Unit 42
- React2Shell: Critical React Vulnerability — Wiz Research
- Critical vulnerability in React and Next.js — VulnCheck
- Summary of CVE-2025-55182 — Vercel
Stay in the Loop!
Be the first to know - subscribe today
Member discussion