diff --git a/src/core/lib/Charts.mjs b/src/core/lib/Charts.mjs
index 6cb63f60..a70a900f 100644
--- a/src/core/lib/Charts.mjs
+++ b/src/core/lib/Charts.mjs
@@ -153,7 +153,7 @@ export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHe
);
let xValues = new Set();
- const series = {};
+ const series = Object.create(null);
values.forEach(row => {
const serie = row[0],
@@ -163,14 +163,14 @@ export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHe
if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10.");
xValues.add(xVal);
- if (typeof series[serie] === "undefined") series[serie] = {};
+ if (typeof series[serie] === "undefined") series[serie] = Object.create(null);
series[serie][xVal] = val;
});
xValues = new Array(...xValues);
const seriesList = [];
- for (const seriesName in series) {
+ for (const seriesName of Object.keys(series)) {
const serie = series[seriesName];
seriesList.push({name: seriesName, data: serie});
}
diff --git a/src/core/lib/Protocol.mjs b/src/core/lib/Protocol.mjs
index dfb8b197..1875e40c 100644
--- a/src/core/lib/Protocol.mjs
+++ b/src/core/lib/Protocol.mjs
@@ -8,6 +8,7 @@
import BigNumber from "bignumber.js";
import {toHexFast} from "../lib/Hex.mjs";
+import Utils from "../Utils.mjs";
/**
* Recursively displays a JSON object as an HTML table
@@ -25,15 +26,16 @@ export function objToTable(obj, nested=false) {
Value |
`;
- for (const key in obj) {
- if (typeof obj[key] === "function")
+ for (const key of Object.keys(obj)) {
+ const value = obj[key];
+ if (typeof value === "function")
continue;
- html += `| ${key} | `;
- if (typeof obj[key] === "object")
- html += `${objToTable(obj[key], true)} | `;
+ html += `
| ${Utils.escapeHtml(String(key))} | `;
+ if (value !== null && typeof value === "object")
+ html += `${objToTable(value, true)} | `;
else
- html += `${obj[key]} | `;
+ html += `${Utils.escapeHtml(String(value))} | `;
html += "
";
}
html += "";
diff --git a/tests/node/index.mjs b/tests/node/index.mjs
index 52670d48..360bf481 100644
--- a/tests/node/index.mjs
+++ b/tests/node/index.mjs
@@ -25,6 +25,7 @@ import "./tests/NodeDish.mjs";
import "./tests/Utils.mjs";
import "./tests/Categories.mjs";
import "./tests/lib/BigIntUtils.mjs";
+import "./tests/lib/ChartsProtocolPrototypePollution.mjs";
const testStatus = {
allTestsPassing: true,
diff --git a/tests/node/tests/lib/ChartsProtocolPrototypePollution.mjs b/tests/node/tests/lib/ChartsProtocolPrototypePollution.mjs
new file mode 100644
index 00000000..be4e7667
--- /dev/null
+++ b/tests/node/tests/lib/ChartsProtocolPrototypePollution.mjs
@@ -0,0 +1,90 @@
+import TestRegister from "../../../lib/TestRegister.mjs";
+import {getSeriesValues} from "../../../../src/core/lib/Charts.mjs";
+import {objToTable} from "../../../../src/core/lib/Protocol.mjs";
+import SeriesChart from "../../../../src/core/operations/SeriesChart.mjs";
+import ParseUDP from "../../../../src/core/operations/ParseUDP.mjs";
+import it from "../../assertionHandler.mjs";
+import assert from "assert";
+
+const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
+
+TestRegister.addApiTests([
+ it("Charts: should not pollute Object.prototype from a __proto__ series name", () => {
+ const xVal = "
";
+ delete Object.prototype[xVal];
+
+ try {
+ const result = getSeriesValues(`__proto__,${xVal},1`, "\n", ",", false);
+
+ assert.equal(Object.prototype[xVal], undefined);
+ assert.deepEqual(result.xValues, [xVal]);
+ assert.equal(result.series.length, 1);
+ assert.equal(result.series[0].name, "__proto__");
+ assert.equal(Object.getPrototypeOf(result.series[0].data), null);
+ assert(hasOwn(result.series[0].data, xVal));
+ assert.equal(result.series[0].data[xVal], 1);
+ } finally {
+ delete Object.prototype[xVal];
+ }
+ }),
+
+ it("Charts: should keep __proto__ x-axis names as own data keys", () => {
+ const result = getSeriesValues("safe,__proto__,1", "\n", ",", false);
+
+ assert.equal(result.series.length, 1);
+ assert.equal(Object.getPrototypeOf(result.series[0].data), null);
+ assert(hasOwn(result.series[0].data, "__proto__"));
+ assert.equal(result.series[0].data.__proto__, 1);
+ }),
+
+ it("Protocol: should ignore inherited properties when rendering tables", () => {
+ const inheritedKey = "
";
+ delete Object.prototype[inheritedKey];
+
+ try {
+ Object.prototype[inheritedKey] = "polluted";
+
+ const html = objToTable({safe: "value"});
+
+ assert(!html.includes(inheritedKey));
+ assert(!html.includes("polluted"));
+ assert(html.includes("safe"));
+ assert(html.includes("value"));
+ } finally {
+ delete Object.prototype[inheritedKey];
+ }
+ }),
+
+ it("Protocol: should escape table keys and scalar values", () => {
+ const obj = {
+ "field": "
",
+ };
+
+ const html = objToTable(obj);
+
+ assert(!html.includes("field"));
+ assert(!html.includes("
"));
+ assert(html.includes("<b>field</b>"));
+ assert(html.includes("<img src=x onerror=alert(1)>"));
+ }),
+
+ it("Series chart and Parse UDP: should not expose polluted prototype data as HTML", () => {
+ const xVal = "
";
+ delete Object.prototype[xVal];
+
+ try {
+ const chartHtml = new SeriesChart().run(
+ `__proto__,${xVal},1`,
+ ["Line feed", "Comma", "", 1, "red"]
+ );
+ assert.equal(Object.prototype[xVal], undefined);
+
+ const parseUDP = new ParseUDP();
+ const tableHtml = parseUDP.present(parseUDP.run(chartHtml, ["Raw"]));
+
+ assert(!/![]()