Prototype Pollution to RCE: A 2026 Exploitation Guide

When I see a bug bounty report for Prototype Pollution, my first thought is usually, "Great, another theoretical DoS." But over the last few weeks, threat actors have repeatedly proven that polluting the JavaScript prototype chain is far more dangerous than just crashing an app. They are chaining these seemingly minor logic flaws directly into Remote Code Execution (RCE).
If you are a defender, ignoring Prototype Pollution because it "lacks impact" is a fast track to a critical breach. Let's break down exactly how attackers are weaponizing this class of vulnerability in 2026, specifically targeting Node.js environments.
The Core Problem: Why Prototype Pollution Exists
In JavaScript, objects inherit properties from their prototype. If an attacker can inject properties into Object.prototype, those properties instantly appear on almost every object across the entire application runtime.
This usually happens during recursive merge operations, like cloning an object or parsing deeply nested JSON payloads. If the logic fails to block the __proto__ key, the attacker gains the ability to poison the global object template.
A Vulnerable Code Pattern
Consider a common, flawed implementation of a deep merge function:
// VULNERABLE CODE - DO NOT USE function merge(target, source) { for (let key in source) { if (typeof source[key] === 'object') { if (!target[key]) target[key] = {}; merge(target[key], source[key]); } else { target[key] = source[key]; } } return target; } // An attacker sends this JSON payload to an API endpoint: const maliciousPayload = JSON.parse('{"__proto__": {"admin": true, "execPath": "calc.exe"}}'); // The backend merges the payload into an empty object merge({}, maliciousPayload); // Now, EVERY object in the application has admin = true const user = {}; console.log(user.admin); // true
While privilege escalation (like the admin = true example) is bad, it requires the application to actually check for user.admin. But what if we want full RCE?
Escalating to RCE via child_process
The real danger in Node.js environments lies in how native modules handle options objects. When you call child_process.spawn() or fork(), Node.js takes an options object to configure the environment variables, working directory, and executable path.
If a developer passes an undefined or partially defined options object, the Node.js runtime falls back to checking the object's prototype.
Imagine you are auditing a server that occasionally spawns a background worker:
const { spawn } = require('child_process'); function runBackgroundJob() { // The options object is intentionally left undefined let options; // Spawns a harmless background worker const p = spawn('/usr/bin/node', ['worker.js'], options); }
If the attacker has already polluted the prototype with env, shell, or NODE_OPTIONS, they can intercept this execution flow. By poisoning the env variable with NODE_OPTIONS=--require /tmp/malicious.js, the attacker forces the Node.js runtime to load their malware before executing worker.js.
Data flow demonstrating how a malicious JSON payload pollutes the global object prototype, hijacking a downstream child_process execution.
AST Injection: The Silent Killer
Another modern RCE vector is Abstract Syntax Tree (AST) injection. Template engines like Pug, Handlebars, and EJS compile templates by building an AST. These compilers heavily rely on configuration objects.
If an attacker pollutes properties like outputFunctionName or blockName, they can break out of the AST compiler and inject raw JavaScript directly into the generated rendering function. This technique, heavily popularized by researchers like Mikhail Shcherbakov, allows attackers to achieve RCE without ever touching child_process.
What This Means for Defenders and Builders
You cannot rely on Web Application Firewalls (WAFs) to catch Prototype Pollution. The payloads are often indistinguishable from legitimate JSON data.
To stop this at the root, you must adopt secure coding practices and leverage modern runtime protections.
1. Freeze the Prototype
The most robust defense is to simply freeze the global object prototype at the start of your application execution.
// Place this at the very top of your entry file (e.g., index.js) Object.freeze(Object.prototype);
While effective, this can break legacy third-party modules that legitimately attempt to modify the prototype. Test thoroughly before deploying to production.
2. Use Safe Map Objects
Stop using {} for dictionaries. If you need a key-value store, use JavaScript's native Map or create objects with a null prototype.
// SAFE: Creates an object that does not inherit from Object.prototype const dict = Object.create(null);
3. Update Dependencies (The Boring but Essential Rule)
Libraries like lodash, qs, and protobufjs have all suffered from severe Prototype Pollution flaws (such as CVE-2022-25878). Ensure you are continuously running dependency scanners.
The Takeaway
Prototype pollution is not a toy bug. It is a stepping stone to complete server compromise. As long as Node.js native modules and template engines rely on unvalidated options objects, attackers will continue to find creative ways to pivot from prototype poisoning to RCE.
If you are interested in diving deeper into secure coding and understanding how hackers exploit misconfigurations, consider checking out our Full Course which covers advanced exploitation techniques.
References / Further reading
- Node.js child_process documentation - Node.js - https://nodejs.org/api/child_process.html
- Prototype Pollution in JavaScript - PortSwigger - https://portswigger.net/web-security/prototype-pollution
- CVE-2022-25878 (protobufjs) - NVD - https://nvd.nist.gov/vuln/detail/CVE-2022-25878
- AST Injection Research - Mikhail Shcherbakov - https://research.securitum.com/prototype-pollution-and-bypassing-client-side-html-sanitizers/


