Skip to main content

Command Palette

Search for a command to run...

React2Shell: The Next Struts2-Style Bug Parade?

Updated
4 min read
React2Shell: The Next Struts2-Style Bug Parade?

The React2Shell bug is giving me major déjà vu, and I think there are important lessons here in Abstract vs. Concrete Risk (maybe in B2B sales too—I haven’t fully thought that part through yet).

In the 2010s, the Apache Struts team made (what appears to be) a similar architectural bet: center a lot of framework behavior around an expression language, OGNL, and use it everywhere. OGNL was woven through Struts with almost no visible guard rails. In the code, there’s no clear module boundary, no “here be dragons” warnings on the API, no strong type or visibility barriers. Just innocuous helper functions like translateVariables() sitting right next to normal framework plumbing, ready to helpfully execute code passed to it.

Over that decade we saw a parade of OGNL-driven RCEs in Struts 2. The same basic pattern kept resurfacing: user-controlled data (which is everywhere in a web framework) was able to reach a code path where it got evaluated as an expression. That led to major breaches like we saw Equifax.

Now fast-forward to React2Shell. React Server Components introduced the Flight protocol and Server Functions: a way to serialize calls from client → server and deserialize them back into function calls and object graphs. The React2Shell vulnerability is essentially an unsafe-deserialization / server-side prototype pollution bug in that decoding logic, which lets an unauthenticated attacker send a crafted payload and end up running code on the server.

It’s already:

  • CVSS 10.0 — things rarely correctly receive this rating.

  • Affecting default configs in React 19 RSC and frameworks like Next.js App Router, React Router RSC, Waku, etc.

  • Being exploited in the wild, by multiple threat actors including China-nexus groups, and has been added to CISA’s KEV catalog.

This is Concrete Risk: there’s a specific CVE (CVE-2025-55182), real exploits, IPs scanning the internet, and a due date on your patch window. Everybody understands this. Everybody is (hopefully) scrambling to patch.

The thing I’m more interested in is the Abstract Risk that was the prelude to this bug:

“We just embedded a powerful little ‘language’ / protocol into the middle of our request pipeline, and it can decide what code runs on the server.”

In Struts, the Abstract Risk looked like: OGNL is an expression language wired directly into request handling. User-controlled strings feed into it from many directions (tags, error messages, parameters…). The framework made it easy to slip from “data” into “code” by design.

In React’s case, the Abstract Risk looks like:

  • The Flight protocol is not “just JSON”; it’s a mini-serialization language for modules, functions, and complex objects.

  • Server Functions sit at a privileged point in your stack – they are your app logic.

  • The decoding logic implicitly expands object properties and hooks into module loading and function invocation. When that layer goes wrong, it goes wrong catastrophically.

You can patch the Concrete Risk with a point release (and you should! React 19.0.1 / 19.1.2 / 19.2.1 and the matching Next.js fixes). But the Abstract Risk question is harder, more important, and hopefully survives the current news cycle:

Do we treat “introducing a new interpreter / protocol in the request path” as a P0 architectural risk class, the way we should have treated OGNL, JNDI, template engines, etc.? Or do we treat each individual CVE as an isolated surprise?

Developers and product teams will almost always fix provable, concrete bugs. There’s a PoC, there’s a KEV entry, there’s a patch. Everyone’s kind of aligned. ✅

But very few teams have a muscle for saying: “This entire pattern of feature is dangerous, even before the first CVE.” “If we ship this, we’re signing up for a decade of whack-a-mole unless we put very strong boundaries around it.” That’s the Abstract Risk: the latent cost of certain architectural choices, whose gene expression could potentially only be revealed as headlines years later. Even now, with a CVE issued, the risk will still feel abstract to the React team. They will feel like they’ve squashed the risk, and any other talk is theoretical. Nothing like taking on the Abstract stuff is discussed in their notice.

I don’t have a hot take about React’s long-term direction here. I don’t know the code well enough. But I do think we can use React2Shell as another data point in a bigger story: whenever we hide an interpreter or a powerful serialization protocol “behind the scenes” of developer ergonomics, history suggests it will eventually surface as remote code execution.

So, what do we do about this now?

Our job in AppSec and platform engineering is to get better at spotting those patterns before the CVEs pile up, and helping communicate how Abstract can become Concrete. The skillset here that will fundamentally affect your success is storytelling, metaphor, choice of battles — not the technical stuff.

If Twain was right, and history will be “rhyming” here — developers won’t perform the full-frontal assault on the Abstract Risk, and so we’ll have to deal with the fallout of many Concrete Risks over the coming years. 🍻