]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Use Object.create(null) as `merge` target, to prevent prototype pollution (#7917)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Sun, 18 Oct 2020 17:31:01 +0000 (20:31 +0300)
committerGitHub <noreply@github.com>
Sun, 18 Oct 2020 17:31:01 +0000 (13:31 -0400)
Use Object.create(null) as merge target to prevent polluting `Object.prototype`

src/core/core.controller.js
src/core/core.datasetController.js
src/core/core.defaults.js
src/core/core.layouts.js
src/helpers/helpers.core.js
src/plugins/plugin.filler.js
src/plugins/plugin.legend.js
src/plugins/plugin.tooltip.js
test/specs/core.helpers.tests.js

index d2df919bccd9e655c4906c9f10f23c74a0bf5df9..0621bc8847ead1224d07b961e71fa8a029b07e3d 100644 (file)
@@ -44,8 +44,8 @@ function mergeScaleConfig(config, options) {
        const chartDefaults = defaults[config.type] || {scales: {}};
        const configScales = options.scales || {};
        const chartIndexAxis = getIndexAxis(config.type, options);
-       const firstIDs = {};
-       const scales = {};
+       const firstIDs = Object.create(null);
+       const scales = Object.create(null);
 
        // First figure out first scale id's per axis.
        Object.keys(configScales).forEach(id => {
@@ -53,12 +53,12 @@ function mergeScaleConfig(config, options) {
                const axis = determineAxis(id, scaleConf);
                const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis);
                firstIDs[axis] = firstIDs[axis] || id;
-               scales[id] = mergeIf({axis}, [scaleConf, chartDefaults.scales[axis], chartDefaults.scales[defaultId]]);
+               scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, chartDefaults.scales[axis], chartDefaults.scales[defaultId]]);
        });
 
        // Backward compatibility
        if (options.scale) {
-               scales[options.scale.id || 'r'] = mergeIf({axis: 'r'}, [options.scale, chartDefaults.scales.r]);
+               scales[options.scale.id || 'r'] = mergeIf(Object.create(null), [{axis: 'r'}, options.scale, chartDefaults.scales.r]);
                firstIDs.r = firstIDs.r || options.scale.id || 'r';
        }
 
@@ -71,7 +71,7 @@ function mergeScaleConfig(config, options) {
                Object.keys(defaultScaleOptions).forEach(defaultID => {
                        const axis = getAxisFromDefaultScaleID(defaultID, indexAxis);
                        const id = dataset[axis + 'AxisID'] || firstIDs[axis] || axis;
-                       scales[id] = scales[id] || {};
+                       scales[id] = scales[id] || Object.create(null);
                        mergeIf(scales[id], [{axis}, configScales[id], defaultScaleOptions[defaultID]]);
                });
        });
@@ -91,7 +91,7 @@ function mergeScaleConfig(config, options) {
  * a deep copy of the result, thus doesn't alter inputs.
  */
 function mergeConfig(...args/* config objects ... */) {
-       return merge({}, args, {
+       return merge(Object.create(null), args, {
                merger(key, target, source, options) {
                        if (key !== 'scales' && key !== 'scale') {
                                _merger(key, target, source, options);
@@ -118,8 +118,8 @@ function initConfig(config) {
 
        options.scales = scaleConfig;
 
-       options.title = (options.title !== false) && merge({}, [defaults.plugins.title, options.title]);
-       options.tooltips = (options.tooltips !== false) && merge({}, [defaults.plugins.tooltip, options.tooltips]);
+       options.title = (options.title !== false) && merge(Object.create(null), [defaults.plugins.title, options.title]);
+       options.tooltips = (options.tooltips !== false) && merge(Object.create(null), [defaults.plugins.tooltip, options.tooltips]);
 
        return config;
 }
index ca26dffc9a4300a4be282e3c611ea67a5d39c796..c57c3ffe1685f52439f70d10bca5e806e38caf07 100644 (file)
@@ -334,7 +334,7 @@ export default class DatasetController {
         */
        configure() {
                const me = this;
-               me._config = merge({}, [
+               me._config = merge(Object.create(null), [
                        me.chart.options[me._type].datasets,
                        me.getDataset(),
                ], {
index 61663fd186cf70f8db781befebb55e43b5a0ae69..6ee9e681e0c9d0fd21cb171256859b4e811ca2ac 100644 (file)
@@ -12,7 +12,7 @@ function getScope(node, key) {
        const keys = key.split('.');
        for (let i = 0, n = keys.length; i < n; ++i) {
                const k = keys[i];
-               node = node[k] || (node[k] = {});
+               node = node[k] || (node[k] = Object.create(null));
        }
        return node;
 }
index 18e2aff7f1c04e96c27f8acef69677e30f8fd987..789731a303f5f02731092c91b0e9653961fb5840 100644 (file)
@@ -1,5 +1,5 @@
 import defaults from './core.defaults';
-import {each} from '../helpers/helpers.core';
+import {each, isObject} from '../helpers/helpers.core';
 import {toPadding} from '../helpers/helpers.options';
 
 /**
@@ -84,6 +84,10 @@ function updateDims(chartArea, params, layout) {
        const box = layout.box;
        const maxPadding = chartArea.maxPadding;
 
+       if (isObject(layout.pos)) {
+               // dynamically placed boxes are not considered
+               return;
+       }
        if (layout.size) {
                // this layout was already counted for, lets first reduce old size
                chartArea[layout.pos] -= layout.size;
index 6aae6c15875af76d7b5c8f7eab91eddc34c9ad35..ec936deb5a860324f0aeca9eaea9eb97c2244c29 100644 (file)
@@ -157,7 +157,7 @@ export function clone(source) {
        }
 
        if (isObject(source)) {
-               const target = {};
+               const target = Object.create(null);
                const keys = Object.keys(source);
                const klen = keys.length;
                let k = 0;
index 49d2d9ec9bb1f4470b35955151e8819f113fbbb0..b70ae53c31289ced0967760f1fe20e716260ed3d 100644 (file)
@@ -55,11 +55,14 @@ function parseFillOption(line) {
  */
 function decodeFill(line, index, count) {
        const fill = parseFillOption(line);
-       let target = parseFloat(fill);
 
        if (isObject(fill)) {
                return isNaN(fill.value) ? false : fill;
-       } else if (isFinite(target) && Math.floor(target) === target) {
+       }
+
+       let target = parseFloat(fill);
+
+       if (isFinite(target) && Math.floor(target) === target) {
                if (fill[0] === '-' || fill[0] === '+') {
                        target = index + target;
                }
index 9f6f894fe7101cdd55f4bce025ec927832709719..c5de3d9c20432e1cd5b592b8cb2d4ac543729366 100644 (file)
@@ -629,7 +629,7 @@ export class Legend extends Element {
 }
 
 function resolveOptions(options) {
-       return options !== false && merge({}, [defaults.plugins.legend, options]);
+       return options !== false && merge(Object.create(null), [defaults.plugins.legend, options]);
 }
 
 function createNewLegendAndAttach(chart, legendOpts) {
index 7b9553c247ef78a213299900b49e098e1d51f9df..58e5fd932e43c361b79575b78a7617e2c70dbf02 100644 (file)
@@ -139,7 +139,7 @@ function createTooltipItem(chart, item) {
  */
 function resolveOptions(options, fallbackFont) {
 
-       options = merge({}, [defaults.plugins.tooltip, options]);
+       options = merge(Object.create(null), [defaults.plugins.tooltip, options]);
 
        options.bodyFont = toFont(options.bodyFont, fallbackFont);
        options.titleFont = toFont(options.titleFont, fallbackFont);
index 17849812573f13bdbe5b80afabdcf9a1cf89f0d3..d48a3a06184de64a3dae71f4b7647c1c504362fe 100644 (file)
@@ -13,4 +13,12 @@ describe('Core helper tests', function() {
                expect(helpers.uid()).toBe(uid + 2);
                expect(helpers.uid()).toBe(uid + 3);
        });
+
+       describe('clone', function() {
+               it('should not allow prototype pollution', function() {
+                       const test = helpers.clone(JSON.parse('{"__proto__":{"polluted": true}}'));
+                       expect(test.prototype).toBeUndefined();
+                       expect(Object.prototype.polluted).toBeUndefined();
+               });
+       });
 });