May 11, 2026·8 min read·7 visits
A missing input validation check in the deepMerge() function of @theecryptochad/merge-guard v1.0.0 permits Prototype Pollution. Attackers can supply a crafted JSON payload containing a `__proto__` key to alter the global Object.prototype. The vulnerability is fixed in version 1.0.1 by implementing a restricted key denylist.
The `@theecryptochad/merge-guard` JavaScript package version 1.0.0 is vulnerable to Prototype Pollution. The `deepMerge()` function fails to validate input keys during recursive object merging, allowing attackers to inject malicious properties into the global `Object.prototype` via the `__proto__` accessor. This widespread environmental state alteration can lead to Denial of Service, business logic bypass, or Remote Code Execution depending on the presence of susceptible gadget chains in the application.
The @theecryptochad/merge-guard package provides functionality for recursively merging JavaScript objects. Developers utilize this utility to combine configuration objects, user input, and default application settings. The core function responsible for this operation is deepMerge(), which accepts a target object and a source object, iterating over the properties of the source to apply them to the target.
In version 1.0.0, the deepMerge() implementation suffers from a Prototype Pollution vulnerability (CWE-1321). The flaw stems from a failure to sanitize or restrict the property keys processed during the recursive merge operation. When the function encounters the __proto__ key, it treats it as a standard object property rather than an internal accessor.
JavaScript resolves property access dynamically through the prototype chain. If a property is not found directly on an object, the engine checks the object's prototype, continuing up the chain until it reaches Object.prototype. Because almost all standard objects inherit from Object.prototype, any modifications made at this level propagate globally across the runtime environment.
Exploitation occurs when an application processes untrusted JSON input containing a __proto__ payload and passes it as the source object to deepMerge(). The function subsequently applies the nested properties directly to Object.prototype. This global state corruption affects all active and future objects within the Node.js process or browser context, establishing the foundation for further exploitation.
The root cause of this vulnerability lies in the unrestricted key iteration within the deepMerge() function. The algorithm uses Object.keys(source) to iterate over the input data and applies a recursive pattern when encountering nested objects. The function does not filter administrative or internal keys during this iteration process.
When an attacker provides an object with a __proto__ key, the condition typeof source[key] === 'object' evaluates to true. The function then executes the recursive call: target[key] = deepMerge(target[key] || {}, source[key]);. In JavaScript, accessing target['__proto__'] returns the actual internal prototype of the target object, which is typically Object.prototype.
The recursive execution then iterates over the properties defined inside the attacker's __proto__ payload. Because the current target context has shifted from the original object to the global Object.prototype, the subsequent assignment operations directly modify the global prototype chain. The engine writes the attacker-controlled keys and values into the base object template used by the entire JavaScript environment.
This behavior is an architectural quirk of JavaScript implementations prioritizing backwards compatibility and dynamic object manipulation. The standard ECMA-262 specifies __proto__ as an accessor property (getter/setter) on Object.prototype. When an assignment occurs via a deep copy operation that does not use safe assignment methods like Object.defineProperty(), the setter invokes and resolves the reference to the underlying prototype.
Analyzing the vulnerable implementation in version 1.0.0 reveals the missing validation step. The function performs a basic type check but immediately proceeds to property assignment.
function deepMerge(target, source) {
if (typeof source !== 'object' || source === null) return target;
for (const key of Object.keys(source)) {
if (
source[key] !== null &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
// Recursion happens here without validating 'key'
target[key] = deepMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}The maintainers resolved this vulnerability in version 1.0.1 by implementing a denylist approach. The patch introduces a BLOCKED_KEYS Set containing __proto__, constructor, and prototype. The iteration logic now explicitly checks if the current key exists in the blocklist and uses the continue statement to skip processing if a match occurs.
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function deepMerge(target, source) {
if (typeof source !== 'object' || source === null) return target;
for (const key of Object.keys(source)) {
// Explicit blocklist check
if (BLOCKED_KEYS.has(key)) continue;
if (
source[key] !== null &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
target[key] = deepMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}The inclusion of constructor and prototype in the denylist is a critical defense-in-depth measure. Attackers often bypass rudimentary __proto__ filters by supplying payloads targeting constructor.prototype. By blocking all three access vectors, the patch effectively mitigates standard prototype pollution techniques against this function. Using a Set for the blocklist also provides O(1) lookup performance, minimizing performance overhead during deep merges.
Exploitation requires the target application to parse user-controlled input and merge it with an existing object using the vulnerable deepMerge() function. The most common vector is a JSON API endpoint that accepts configuration updates or profile data. The attacker constructs a JSON string containing the malicious payload and submits it to the application.
// Proof of Concept Demonstration
const { deepMerge } = require('@theecryptochad/merge-guard');
const untrustedInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
const safeObject = {};
deepMerge(safeObject, untrustedInput);
const newObject = {};
console.log(newObject.isAdmin); // Output: trueThe use of JSON.parse() is a crucial detail in this attack vector. Native object literals in JavaScript execute the __proto__ setter during creation, meaning const obj = { __proto__: { a: 1 } } simply changes obj's prototype without retaining __proto__ as an enumerable string key. However, JSON.parse() creates objects where __proto__ exists as a standard, enumerable property. When deepMerge() processes this parsed object, Object.keys() successfully extracts the __proto__ string, initiating the exploit chain.
Once the global prototype is polluted, the attacker relies on "gadgets" within the application code or its dependencies. A gadget is an existing code segment that accesses an undefined object property and acts upon the retrieved value. If the attacker pollutes the prototype with that specific property name, the gadget accesses the injected malicious value instead of undefined.
The impact of a Prototype Pollution vulnerability depends entirely on the surrounding application context. Because the modification affects the global execution environment, the blast radius spans the entire Node.js process or browser session. Every object instantiated after the exploitation, and every existing object that relies on the prototype chain, inherits the polluted properties.
Denial of Service (DoS) is the most immediate and common consequence. An attacker can overwrite fundamental JavaScript methods such as toString, valueOf, or hasOwnProperty with incompatible types (e.g., an integer or a string). When the application implicitly or explicitly invokes these methods during normal operation, the JavaScript engine throws an unhandled TypeError, resulting in an immediate process crash.
Logic bypass occurs when the application relies on uninitialized object properties to control execution flow. If an authorization middleware checks if (user.isAdmin) without explicitly validating the property's existence on the instance itself, the injected prototype value acts as a fallback. The attacker successfully escalates privileges across the application by simply establishing a default truthy value on the prototype.
Remote Code Execution (RCE) represents the most severe outcome. RCE requires the presence of a specific gadget chain, typically found in process execution libraries, templating engines, or child process generation. For example, if the application utilizes child_process.spawn() with an options object that does not strictly define the env or shell properties, an attacker can inject an environment variable payload or force execution through a shell to run arbitrary system commands.
The primary remediation strategy is upgrading the @theecryptochad/merge-guard package to version 1.0.1 or later. This release contains the necessary structural changes to block prototype pollution vectors natively. Development teams must update their package.json dependencies and execute the package manager update command to integrate the patched version.
If upgrading is temporarily impossible, developers can implement input sanitization before passing objects to deepMerge(). Removing __proto__, constructor, and prototype keys from untrusted input mitigates the vector. Additionally, utilizing JSON Schema validation tools (such as Ajv) configured to reject unknown or specific internal properties provides a robust perimeter defense against malformed inputs.
System-wide defense-in-depth measures can prevent the exploitation of Prototype Pollution vulnerabilities globally. Running Node.js applications with the --disable-proto=delete CLI flag completely removes the __proto__ accessor from Object.prototype. Alternatively, executing Object.freeze(Object.prototype) during application initialization permanently locks the global prototype, preventing any downstream modifications regardless of vulnerable merge implementations.
When developing custom object manipulation functions, developers should avoid relying exclusively on standard object literals. Instantiating objects via Object.create(null) generates a dictionary without an inherited prototype chain. Merging into prototype-less objects neutralizes standard pollution attacks, as the __proto__ property does not resolve to the global accessor.
| Product | Affected Versions | Fixed Version |
|---|---|---|
@theecryptochad/merge-guard TheeCryptoChad | < 1.0.1 | 1.0.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-1321 |
| Attack Vector | Network |
| Estimated CVSS | 9.8 |
| Impact | DoS, Logic Bypass, RCE |
| Exploit Status | Proof of Concept Available |
| Vulnerable Component | deepMerge() function |
The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.