React Router prototype pollution chained into RCE: CVE-2026-42211

turbo-stream v2 calls a constructor while rehydrating the TYPE_ERROR branch without checking where it came from. If the app already has a prototype pollution bug, that path reaches Function and two moderate flaws become RCE.

Overview

Field Detail
CVE CVE-2026-42211
CVSS 8.1 (High)
CWE CWE-502 deserialisation of untrusted data
Affected React Router 7.0.0 to 7.14.1, Framework Mode only
Fixed in 7.14.2
Disclosed 2026-06-03
Exploited in the wild No public reports

React Router is the most widely used routing library in the React ecosystem, downloaded over 120 million times a week. The flaw disclosed on 3 June 2026 has a defining property: it cannot be triggered on its own. It needs prototype pollution plus unsafe deserialisation, in that order.

The exposure is what makes it serious. Framework Mode is the officially recommended default, so most React full-stack projects created after October 2024 carry this attack surface by default. After disclosure it briefly topped the CVE Intruder trending list.

How it was found

The researcher @securityMB, at the GitHub Security Lab, found it while auditing turbo-stream v2, the serialisation library embedded in Framework Mode.

In normal operation the TYPE_ERROR branch only rebuilds a TypeError object. But the branch calls a constructor to rehydrate the data without validating where that constructor came from. So if an attacker can inject properties into the Error.prototype chain, through any independent prototype pollution bug in the application, the expected constructor can be replaced with any constructor in the environment, Function among them.

And Function executes JavaScript directly when handed a string argument. The last link closes there.

Reproduction

Everything below is for authorised security testing only.

Build an affected version with the official scaffolding:

npx create-react-router@7.14.1 vulnerable-app
cd vulnerable-app && npm install && npm run dev

Step one: establish the precondition. A lab reproduces the leftover jQuery or old lodash pattern with an unsafe deep merge:

function deepMerge(t, s) {
  for (const key in s) {
    if (typeof s[key] === 'object' && s[key] !== null) {
      t[key] = t[key] || {};
      deepMerge(t[key], s[key]);
    } else {
      t[key] = s[key];
    }
  }
}

It treats __proto__ and constructor.prototype as ordinary keys and writes them through, which is exactly the way in.

Step two: trigger the deserialiser. With the pollution in place, send a request carrying crafted turbo-stream data to a Framework Mode data endpoint so the flow reaches the TYPE_ERROR branch. The polluted constructor then stands in for the expected type, Function is invoked, and the supplied string is code. The published PoC builds that structure in Python; one command verifies it:

python3 poc_cve_2026_42211.py "id"

Step three: confirm. Expect the output of the server process identity, something like uid=1000(node), which proves the code ran on the server rather than appearing in a forged response.

Fix

The fix is 7.14.2. The patch removes the undocumented custom error serialisation logic, cutting the chain at its source:

npm install react-router@7.14.2
npm list react-router

If upgrading is not immediate, fall back to declarative mode (<BrowserRouter>) or data mode (createBrowserRouter / <RouterProvider>), neither of which is affected, and narrow the external reachability of Framework Mode endpoints.

Finally, hunt the pollution itself. Point dependency scanning at every deep merge path ($.extend(true, ...), _.merge and friends) and reject __proto__, constructor and prototype before merging. Leave that precondition standing and the same class of bug returns in another shape.

Verdict

CVE-2026-42211 is arithmetic. Prototype pollution is low severity alone, unsafe deserialisation is moderate alone, and chained they are remote code execution. Scoring findings one at a time is not enough; whether two findings can be stacked is part of the risk, and that composition stays invisible in every individual score.

← Back to all posts

Comments

…