The art of prototype pollution gadgets in NodeJS
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.
Quick check, is this code safe?
//server.mjs
import express from 'express';
const app = express();
app.use(express.json());
function unsafeMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {};
unsafeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
app.post('/try', async (req, res) => {
const userInput = req.body || {};
unsafeMerge({}, userInput);
await import('./someLIB.mjs');
res.send('ok');
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));the answer is.. no this code is not safe and it has simple prototype pollution (basic question)
but how we can get highly impact for this code, if you see this code doesnt has a useful gadget and simple code you are wrong
if this code run by node js and express js thats means we can get gadget from node js code or express js code (its need deep search im ngl)
in this article i would take gadget from internal node js source code specifically esm module code
defaultLoad (load.js)
the source code for what i will talk about:
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
sourceis not provided in thecontext, it callsgetSourceSync()to read the module from disk or data URLs.

https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/load.js#L32
After retrieving the source, it invokes
defaultGetFormat()to detect the module type (e.g.,esm,commonjs,builtin).If format is detected as
commonjs, the loader discardssourceto trigger a CJS re-fetch.It validates
importAttributesand returns an object withformat,responseURL, andsource.
Key Observation
defaultLoaddynamically spreadscontextinto a new object with__proto__:

https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/load.js#L89
If Object.prototype.source is polluted, this value is implicitly trusted as the module’s code.
When
formatisn’tcommonjs, Node’s loader skips re-reading the file and may execute this injectedsourcedirectly.This turns a simple prototype pollution bug into a code execution primitive through the ESM loader.
Observation
The critical detail in defaultLoad is how it constructs a new context object with a __proto__ reference:

https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/load.js#L89
This line means:
If
Object.prototype.sourceis polluted, the loader now inherits that attacker-controlledsource.The loader trusts this
sourcevalue to contain module content.Instead of reading a file via
getSourceSync, Node may directly return the pollutedsource, effectively running injected JavaScript.
This creates a dangerous scenario:
A single prototype pollution (
__proto__.source = <payload>) is enough.Any
import()call for a non-commonjsmodule 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 poisonObject.prototype:The server merges the payload, setting
Object.prototype.source.import('./someLIB.mjs')triggersdefaultLoad, which reads the pollutedcontext.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
Last updated