in this article, I will deep dive in some of prototype pollution gadgets in Node.js, sharing my personal journey and a deep dive into one gadget with insights from Node.js internals
Overview
Node.js uses the ESM loader to dynamically import modules at runtime.
The loader relies on plain objects (context) to track module metadata such as source, format, and importAttributes.
Because these objects inherit from Object.prototype, prototype pollution can inject unexpected properties, which may lead to arbitrary code execution during a dynamic import().
This article analyzes the relevant internals (defaultLoad and getSourceSync), explains why prototype pollution affects them, and demonstrates the potential exploit.
defaultLoad is the core function in Node.js' ESM loader responsible for resolving and loading modules. It processes a module specifier (url) and context (importAttributes, format, source, etc.), then determines how to fetch and interpret the module.
Key behavior:
If source is not provided in the context, it calls getSourceSync() to read the module from disk or data URLs.
If Object.prototype.source is polluted, the loader now inherits that attacker-controlled source.
The loader trusts this source value to contain module content.
Instead of reading a file via getSourceSync, Node may directly return the polluted source, effectively running injected JavaScript.
This creates a dangerous scenario:
A single prototype pollution (__proto__.source = <payload>) is enough.
Any import() call for a non-commonjs module may execute arbitrary attacker-supplied code.
In essence, defaultLoad’s prototype inheritance pattern gives attackers a direct path from prototype pollution to remote code execution (RCE) in Node.js.
Exploit Concept
An attacker can abuse the unsafe deep merge logic to pollute Object.prototype with a source property containing malicious JavaScript code. When Node’s ESM loader (defaultLoad) is invoked to import a module, it checks context.source first. Since this property is inherited through prototype pollution, the loader treats the attacker-supplied string as the module source code and executes it.
Exploitation Flow:
Send a crafted request with __proto__ keys to poison Object.prototype:
The server merges the payload, setting Object.prototype.source.
import('./someLIB.mjs') triggers defaultLoad, which reads the polluted context.source.
Node executes attacker-controlled code instead of loading the real module.
Impact: Remote Code Execution (RCE) with full control over the Node.js process.
Conclusion
This vulnerability demonstrates how unsafe object merging can escalate into full Remote Code Execution (RCE) when combined with Node.js internals. By polluting Object.prototype, an attacker can hijack the ESM loader’s module resolution flow and execute arbitrary code. The flaw highlights why prototype pollution is not just a data integrity issue but a critical security risk in any application or framework that dynamically loads code.
Reference
Many popular libraries ship with known server-side prototype pollution gadgets, see