On December 3, 2025, the React team disclosed CVE-2025-55182 — "React2Shell" — a pre-authentication remote code execution vulnerability in React Server Components and Next.js. CVSS score: 10.0, the maximum. By December 5, two days later, exploitation attempts were observed in the wild. The vulnerability sits at the root of how the modern JavaScript ecosystem deserializes data between client and server. It is a prototype pollution bug.
That disclosure is the largest, most public reminder of a quiet truth: prototype pollution is the bug class that the JavaScript ecosystem cannot seem to outgrow. It is in lodash (4 million GitHub projects depend on it). It is in Axios (CVSS 10.0, disclosed 2026 with an AWS metadata-bypass gadget chain). It is in MongoDB's official driver, in expr-eval, in dozens of utility packages your team installed without auditing. And if you ship a Node.js backend that accepts JSON, there is a real and measurable chance you have one right now.
This article is written for two readers. If you are a founder or CTO whose product runs on Node.js, the first sections explain what is actually at stake — in supply-chain and breach-cost terms — and the questions to put to your team. If you are an engineer, the technical sections cover the exploitation mechanics, the recent (2024–2026) CVE landscape, the gadget-chain research published at USENIX, and the defenses that actually work end-to-end.
The 60-second version for non-technical leaders
JavaScript has a feature called "prototypes" — a shared template that every object in the program inherits properties from. If an attacker can modify that shared template by sending crafted JSON to your API, every object in your application is silently changed. Authentication checks that should fail now pass. Default values flip. Code paths that should never execute, execute. In the worst case, the attacker's input becomes code that runs on your server.
The reason this matters at the executive level:
- Your stack is the target. If you run Node.js — and most modern SaaS does — this bug class applies to you. The 2023 USENIX paper "Silent Spring" demonstrated automated end-to-end RCE chains in Node.js. The 2024 follow-up paper "GHUNTER" identified 123 universal gadgets in the Node.js and Deno runtimes themselves — meaning the exploitation surface is built into the platform, not just third-party libraries.
- It rides in via your dependencies. A typical Node.js app pulls in 1,000+ transitive npm packages. A bug in any one of them — a single deep-merge utility published by a developer you've never heard of — can pollute your runtime. Snyk and GitHub track new prototype-pollution CVEs every quarter. 2025 alone produced new CVEs in lodash (the most-downloaded utility on npm), Axios (the most-downloaded HTTP client), and Next.js / React Server Components (one of the most-used SaaS web frameworks).
- The economics match other RCE bugs. IBM's 2025 Cost of a Data Breach Report puts the global average at $4.44M. RCE-class vulnerabilities — where an attacker runs arbitrary code on your server — sit at the top of every breach-cost decile, often leading to ransomware, data exfiltration, and cloud-account compromise. The Axios CVE-2026-40175 disclosure included a working chain from prototype pollution to AWS IMDSv2 credential theft — a full cloud takeover from a single JSON payload.
- Automated scanners do not find it. This is the part most product leaders get wrong. Snyk, Dependabot, and SAST tools flag the libraries that contain prototype pollution — but they do not flag the application code that uses those libraries unsafely, or the application's own home-grown merge functions. Finding the bug in your code requires manual review.
Translation: if you ship a Node.js or React application and your last security assessment did not specifically include a prototype-pollution test against both your dependencies and your own code, you are operating with the same risk profile as the companies whose names are about to appear in next quarter's CVE database.
Why JavaScript has this bug and other languages don't
Most authentication or RCE bugs in other ecosystems require either a memory-safety failure (C/C++/Rust unsafe) or a serialization quirk (Java/Python deserialization). Prototype pollution is structurally different. It exploits a deliberate feature of JavaScript: prototype-based inheritance.
In JavaScript, every object has a hidden link to a "prototype" object. When you read a property on an object and it isn't there, JavaScript silently walks up to the prototype and reads it from there. If you change a property on the prototype, every object that inherits from it now sees the change.
For the most universal prototype — Object.prototype — that means every object in the program. Modify Object.prototype.isAdmin = true, and every object in your application now reports isAdmin === true unless it explicitly defines otherwise.
The attacker's job is to find a code path that takes their JSON input and merges it into a server-side object using a naive recursive function. The magic key __proto__ in their input writes through to the actual prototype object. That is the entire mechanism. Everything else is figuring out what the new defaults do.
The basic exploit — copy-pasteable
The classic vulnerable pattern:
// Vulnerable: a naive recursive merge
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
merge(target[key] || (target[key] = {}), source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Application code:
app.post('/settings', (req, res) => {
const userSettings = {};
merge(userSettings, req.body); // attacker controls req.body
saveSettings(req.user.id, userSettings);
res.json({ ok: true });
});
The attacker sends:
POST /settings
Content-Type: application/json
{"__proto__": {"isAdmin": true, "polluted": "yes"}}
After this request returns, every object in the Node.js process — including the one your auth middleware will construct for the next request, including the one your template engine will use, including the one your database driver will use — has isAdmin: true and polluted: "yes" as inherited properties.
What happens next depends entirely on which "gadget" exists in your code or your dependencies. And in modern Node.js applications, there are usually many.
Attack 1 — Authentication bypass via polluted defaults
What an attacker does, in plain English: sends one JSON payload that pollutes a property the auth middleware checks. Every subsequent request — including ones from unauthenticated users — passes the auth check.
Business impact: full authentication bypass across the entire process for as long as it stays running. Restart resets the pollution, but a single payload re-applies it. Stealthy: nothing in the logs shows an exploit.
Real-world pattern we find. The auth middleware checks req.user.isAdmin. The user object is populated from session data — admins have isAdmin: true, regular users have nothing set. An attacker pollutes Object.prototype.isAdmin = true. Every user object now reads true when the property is checked, even when nothing in the session sets it.
Attack 2 — Remote code execution via gadget chains
What an attacker does, in plain English: pollutes a specific property that some other code path uses as a configuration option. That "config option" turns out to be a path to a script, a shell argument, or a function reference. The application happily uses the polluted value as code.
Business impact: full server compromise. Attacker runs arbitrary commands as your Node.js process — which usually means as the same user that has access to your database credentials, cloud IAM role, and customer data.
The famous gadget: NODE_OPTIONS in child_process. When Node.js spawns a child process (via spawn, exec, or fork), it inherits environment variables. If an attacker can pollute the prototype to inject an env object whose NODE_OPTIONS value contains --require /tmp/evil.js, the next child process loads attacker-controlled code at startup.
// Attacker payload (single JSON POST):
{
"__proto__": {
"env": { "NODE_OPTIONS": "--require /proc/self/environ" }
}
}
// Result: any subsequent child_process.spawn() in the app loads
// attacker-supplied code as the new process initializes.
The 2023 USENIX paper "Silent Spring" published end-to-end exploits using exactly this primitive. The 2024 "GHUNTER" paper extended it: 123 universal gadgets identified in the Node.js and Deno runtimes themselves, available regardless of which third-party libraries you use.
Real CVEs in this class — 2024 to 2026:
- CVE-2025-55182 — React2Shell. CVSS 10.0. Pre-authentication RCE in React Server Components and Next.js. The Flight protocol that ships data from client to server deserializes JSON without rejecting
__proto__andconstructorkeys. Active exploitation observed within 48 hours of disclosure (December 3 → December 5, 2025). Microsoft, Akamai, Datadog, and Picus all published advisories within the first week. - CVE-2025-66478 — Next.js. Companion CVE to React2Shell, working exploit published by Praetorian. Affects production Next.js deployments running React Server Components.
- CVE-2026-40175 — Axios. CVSS 10.0. Prototype pollution gadget chain in the most-downloaded HTTP client in the npm ecosystem. The disclosed exploit includes a working AWS IMDSv2 bypass — direct path from a single JSON payload to full AWS account compromise via stolen IAM credentials. This is the worst-case business-impact scenario in one CVE.
- CVE-2025-13204 — expr-eval. Prototype pollution to RCE in a popular npm expression-evaluation package. Often pulled in transitively by analytics, dashboards, and formula-rendering libraries.
- MongoDB driver 6.6.2. Researcher-disclosed gadget chain from prototype pollution to RCE in the official Node.js driver for MongoDB — meaning any pollution upstream in a typical MEAN-stack app could land code execution via the database layer.
Attack 3 — Client-side XSS via DOM gadgets
What an attacker does, in plain English: exploits prototype pollution in client-side JavaScript to make the browser execute their script. Often via a URL parameter that the page deserializes.
Business impact: account takeover via stolen session tokens, malicious form actions, redirect to phishing. Easier to exploit at scale because attacker only needs to send a link.
Real history: jQuery 3.0–3.3.x shipped a prototype pollution bug (CVE-2019-11358) that affected hundreds of millions of websites for years. Researcher Johan Carlsson published a 2024 walkthrough demonstrating gadget chains in jQuery that turn the bug into stored XSS on real web properties. Lodash CVE-2019-10744 — and the more recent CVE-2025-13465 and CVE-2026-2950 — affect every project pinned to old versions, which is most of them.
Attack 4 — Denial of service via polluted core methods
What an attacker does, in plain English: pollutes a method that every object in the program uses (toString, hasOwnProperty, etc.) with a value that throws an exception.
Business impact: the application crashes on every request that touches a polluted object, in an infinite loop, until restarted. A single JSON payload can take down the production process.
This is the lowest-impact variant in the catalog — but the easiest to exploit and the hardest to detect, because the symptoms look like a runtime bug, not an attack.
Where we find the bugs
The bug-prone code paths in your application and dependencies:
- JSON body parsers that recursively merge user input into a server-side object. Custom middleware is the worst offender.
- Query-string parsers that build nested objects from
?a[b][c]=1-style input.qs, older versions ofexpress, and similar. - Deep-clone or deep-merge utilities.
lodash's_.merge/_.defaultsDeep/_.sethad it.jQuery's$.extend(true, …)had it.merge-deep,deepmerge, dozens of ad-hoc implementations still have it. - YAML and TOML parsers that respect merge keys (
<<). - Config loaders that combine env, files, and request input without sanitization.
- Form parsers on the client side, including older
jQueryform serialization and several React form libraries. - RPC and serialization protocols. React Server Components Flight protocol (the React2Shell vector), gRPC-web JSON fallbacks, several GraphQL extensions.
The defenses that actually work
- Use
Object.create(null)for any object built from untrusted input. An object created this way has no prototype, so prototype pollution literally has no target. This is the single most effective structural defense. - Validate incoming JSON against a strict schema before deserializing it into application state. Zod, Joi, ajv — pick one and apply it on every endpoint. A schema that rejects unknown properties stops
__proto__at the door. - Freeze the prototype at startup.
Object.freeze(Object.prototype)ends the entire bug class for the process. Heavy-handed — some libraries break — but a legitimate option for security-critical services where the trade-off is worth it. - Block dangerous keys at the parser layer. Reject
__proto__,constructor, andprototypeas object keys in any deserialization that produces application objects. Many modern libraries do this by default; check yours. - Patch your dependencies. The merge libraries patched this between 2019 and 2022; if you're pinned to old versions, upgrade. For the 2025–2026 wave — Axios, lodash, React/Next.js, MongoDB driver, expr-eval — pin to the patched releases immediately.
- TypeScript does not help. Type definitions are erased at runtime. The runtime is JavaScript. Prototype pollution is a runtime concern. A team's confidence that "we're on TypeScript" is one of the more reliable false-security signals we encounter.
- Move tasks that need a clean environment to a separate process. If you spawn child processes, scrub the environment first. The
NODE_OPTIONSgadget is closed if you do not let user-controlled keys reachenv.
The boardroom translation table
| What your team says | What it means | What it could cost you |
|---|---|---|
| "We're on Node.js and we use lodash / Axios / Next.js" | Your stack contains libraries with critical 2025–2026 RCE CVEs. Patch cadence is the question. | RCE-class breach. IBM average: $4.44M. Cloud takeover possible via Axios chain. |
| "We use TypeScript so we're safe" | TypeScript is a compile-time tool. Prototype pollution is runtime. | The same exposure as plain JS, with extra confidence that it doesn't apply. |
"Our auth middleware checks req.user.isAdmin" |
One polluted prototype = every request authenticates as admin until restart. | Full data exposure for the lifetime of the process. |
| "We use a custom JSON body parser" | You have rolled your own deserializer. Almost always a finding. | Almost certain finding in any external assessment. |
| "Dependabot says we're clean" | Dependabot flags libraries with known CVEs. It does not find prototype-pollution sinks in your own code, or unpublished gadgets in dependencies. | False sense of coverage. Real bugs sit underneath. |
Five questions a non-technical leader should ask the engineering team
- "What versions of lodash, Axios, Next.js, and the MongoDB driver are we running?" If any of these are unpinned, more than a year old, or behind the patched releases for the 2025–2026 CVEs, that's a finding before the pentest starts.
- "Have we audited our application code for unsafe deep-merge or recursive-merge patterns?" The right answer is "yes, with grep for
__proto__,constructor, and recursive merge patterns." If the answer is "Dependabot covers that," dig further — it does not. - "Do we validate every incoming JSON payload against a strict schema, including rejecting unknown keys?" If not, you are accepting
__proto__as a valid key. - "How long does a prototype pollution payload persist in our running processes?" The honest answer is "until restart" — meaning a single payload can have hours or days of impact on a long-running service.
- "Can we measure our exposure to the Axios IMDSv2 gadget chain in our AWS environment?" If the answer is "we have no telemetry on outbound calls to
169.254.169.254," your detection has the same blind spot the attackers count on.
What we test, every Node.js engagement
- JSON body endpoints:
{"__proto__": {"polluted": "x"}},{"constructor": {"prototype": {"polluted": "x"}}}, then verify pollution via a downstream endpoint. - Query-string endpoints:
?__proto__[polluted]=x,?constructor[prototype][polluted]=x. - YAML / TOML / form-data deserialization paths with the same payloads.
- Gadget hunting: known Node.js / Deno gadgets (GHUNTER catalog), runtime-version-specific RCE chains, child-process
NODE_OPTIONSabuse. - Library-specific gadgets: lodash, Axios, MongoDB driver, Mongoose, ejs/Pug/Handlebars, express middleware.
- Auth bypass via polluted defaults (
isAdmin,verified, role booleans). - Client-side gadget hunting in jQuery, internal SDKs, and bundled React components.
- DoS via polluted
toString,hasOwnProperty. - React Server Components Flight protocol payloads (CVE-2025-55182 class).
If your last assessment did not specifically test against the 2025–2026 catalog — React2Shell, the Axios IMDSv2 chain, the lodash and MongoDB gadgets — your Node.js application has not been pressure-tested against the current attacker.
What makes this bug class durable
Prototype pollution sits between two engineering disciplines. From a security testing perspective, it is hard to find without understanding the application's data flow. From a development perspective, it is invisible during normal use — the application works perfectly until someone sends a magic key. From a tooling perspective, automated scanners can flag known-vulnerable library versions but cannot identify your own unsafe sinks or unpublished gadget chains.
For us, it has become a standard part of every assessment that includes a Node.js backend or a React frontend. We almost always find at least one entry point. On larger codebases — especially ones that built their own request-parsing or config-merging utilities — we sometimes find dozens.
The bottom line
Prototype pollution is the bug class JavaScript cannot quite shake. It is in the runtime (GHUNTER's 123 gadgets), in the most-downloaded libraries (lodash, Axios), in the modern frameworks (React Server Components, Next.js), and in the application code your team wrote yesterday. The blast radius is RCE-class — full server compromise, cloud-IAM takeover, ransomware staging.
The defenses are well-understood and inexpensive: Object.create(null), strict input schemas, patched dependencies, and a few hours of focused testing against your own deserialization paths. The cost of getting them right this quarter is meaningfully smaller than the cost of having your name in the next React2Shell-shaped headline.
If your team has not specifically tested for prototype pollution in the last twelve months — across both dependencies and your own code — your application is operating with the risk profile that produced the last wave of CVE-10.0 disclosures. The bug class is durable. The fix is short. The conversation is worth having now.