From c41ef185136a7b96ca1049c7745a7503b82193de Mon Sep 17 00:00:00 2001 From: Andrew Coleman Date: Fri, 15 May 2026 14:45:31 +0100 Subject: [PATCH 1/2] Prevent object prototype pollution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In line with best practices described here: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution the following changes have been made across the codebase to prevent potential object prototype pollution: - All objects created using `… = {}` have been changed to `... = Object.create(null)` (i.e.) null prototype objects. - All occurrencies of `obj.hasOwnProperty(‘prop’)` have been changed to `Object.prototype.hasOwnProperty.call(obj, ‘prop’)` - All occurrencies of `for(const prop in obj)` have been changed to `for(const prop of Object.keys(obj))` - Changed arr.forEach(…) to Array.prototype.forEach.call(arr, …) when iterating over input sequences. Signed-off-by: Andrew Coleman --- src/datetime.js | 10 +++++----- src/functions.js | 16 ++++++++-------- src/jsonata.js | 38 +++++++++++++++++++------------------- src/parser.js | 2 +- src/signature.js | 4 ++-- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/datetime.js b/src/datetime.js index ba01a110..3710ac4a 100644 --- a/src/datetime.js +++ b/src/datetime.js @@ -74,7 +74,7 @@ const dateTime = (function () { return words; } - const wordValues = {}; + const wordValues = Object.create(null); few.forEach(function (word, index) { wordValues[word.toLowerCase()] = index; }); @@ -952,11 +952,11 @@ const dateTime = (function () { * @returns {object} - regex */ function generateRegex(formatSpec) { - var matcher = {}; + var matcher = Object.create(null); if (formatSpec.type === 'datetime') { matcher.type = 'datetime'; matcher.parts = formatSpec.parts.map(function (part) { - var res = {}; + var res = Object.create(null); if (part.type === 'literal') { res.regex = part.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } else if (part.component === 'Z' || part.component === 'z') { @@ -1004,7 +1004,7 @@ const dateTime = (function () { } else { // must be a month or day name res.regex = '[a-zA-Z]+'; - var lookup = {}; + var lookup = Object.create(null); if (part.component === 'M' || part.component === 'x') { // months months.forEach(function (name, index) { @@ -1187,7 +1187,7 @@ const dateTime = (function () { const tmA = 23; // binary 010111 const tmB = 47; // binary 101111 - const components = {}; + const components = Object.create(null); for (let i = 1; i < info.length; i++) { const mpart = matchSpec.parts[i - 1]; if (mpart.parse) { diff --git a/src/functions.js b/src/functions.js index bb6ebf66..e1284986 100644 --- a/src/functions.js +++ b/src/functions.js @@ -1662,7 +1662,7 @@ const functions = (() => { if (Array.isArray(arg)) { // merge the keys of all of the items in the array - var merge = {}; + var merge = Object.create(null); for(var ii = 0; ii < arg.length; ii++) { var allkeys = keys.call(this, arg[ii]); allkeys.forEach(function (key) { @@ -1754,8 +1754,8 @@ const functions = (() => { result = append.call(this, result, spread.call(this, arg[ii])); } } else if (arg !== null && typeof arg === 'object' && !isLambda(arg)) { - for (var key in arg) { - var obj = {}; + for (const key of Object.keys(arg)) { + var obj = Object.create(null); obj[key] = arg[key]; result.push(obj); } @@ -1777,10 +1777,10 @@ const functions = (() => { return undefined; } - var result = {}; + var result = Object.create(null); arg.forEach(function (obj) { - for (var prop in obj) { + for (const prop of Object.keys(obj)) { result[prop] = obj[prop]; } }); @@ -1820,7 +1820,7 @@ const functions = (() => { async function each(obj, func) { var result = this.createSequence(); - for (var key in obj) { + for (const key of Object.keys(obj)) { var func_args = hofFuncArgs(func, obj[key], key, obj); // invoke func var val = await func.apply(this, func_args); @@ -2047,9 +2047,9 @@ const functions = (() => { * @returns {object} - sifted object */ async function sift(arg, func) { - var result = {}; + var result = Object.create(null); - for (var item in arg) { + for (const item of Object.keys(arg)) { var entry = arg[item]; var func_args = hofFuncArgs(func, entry, item, arg); // invoke func diff --git a/src/jsonata.js b/src/jsonata.js index 5f9bf2c7..c8057551 100644 --- a/src/jsonata.js +++ b/src/jsonata.js @@ -219,7 +219,7 @@ var jsonata = (function() { resultSequence.keepSingleton = true; } - if (expr.hasOwnProperty('group')) { + if (Object.prototype.hasOwnProperty.call(expr, 'group')) { resultSequence = await evaluateGroupExpression(expr.group, isTupleStream ? tupleBindings : resultSequence, environment) } @@ -228,7 +228,7 @@ var jsonata = (function() { function createFrameFromTuple(environment, tuple) { var frame = createFrame(environment); - for(const prop in tuple) { + for(const prop of Object.keys(tuple)) { frame.bind(prop, tuple[prop]); } return frame; @@ -271,13 +271,13 @@ var jsonata = (function() { resultSequence = result[0]; } else { // flatten the sequence - result.forEach(function(res) { + Array.prototype.forEach.call(result, function(res) { if (!Array.isArray(res) || res.cons) { // it's not an array - just push into the result sequence resultSequence.push(res); } else { // res is a sequence - flatten it into the parent sequence - res.forEach(val => resultSequence.push(val)); + Array.prototype.forEach.call(res, val => resultSequence.push(val)); } }); } @@ -349,7 +349,7 @@ var jsonata = (function() { res = [res]; } for (var bb = 0; bb < res.length; bb++) { - tuple = {}; + tuple = Object.create(null); Object.assign(tuple, tupleBindings[ee]); if(res.tupleStream) { Object.assign(tuple, res[bb]); @@ -422,7 +422,7 @@ var jsonata = (function() { res = [res]; } if (isArrayOfNumbers(res)) { - res.forEach(function (ires) { + Array.prototype.forEach.call(res, function (ires) { // round it down var ii = Math.floor(ires); if (ii < 0) { @@ -633,7 +633,7 @@ var jsonata = (function() { flattened = []; } if(Array.isArray(arg)) { - arg.forEach(function (item) { + Array.prototype.forEach.call(arg, function (item) { flatten(item, flattened); }); } else { @@ -674,7 +674,7 @@ var jsonata = (function() { results.push(input); } if (Array.isArray(input)) { - input.forEach(function (member) { + Array.prototype.forEach.call(input, function (member) { recurseDescendants(member, results); }); } else if (input !== null && typeof input === 'object') { @@ -909,8 +909,8 @@ var jsonata = (function() { * @returns {{}} Evaluated input data */ async function evaluateGroupExpression(expr, input, environment) { - var result = {}; - var groups = {}; + var result = Object.create(null); + var groups = Object.create(null); var reduce = input && input.tupleStream ? true : false; var focus = { createSequence: environment.base.createSequence @@ -942,7 +942,7 @@ var jsonata = (function() { if (key !== undefined) { var entry = {data: item, exprIndex: pairIndex}; - if (groups.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(groups, key)) { // a value already exists in this slot if(groups[key].exprIndex !== pairIndex) { // this key has been generated by another expression in this group @@ -993,13 +993,13 @@ var jsonata = (function() { if(!Array.isArray(tupleStream)) { return tupleStream; } - var result = {}; + var result = Object.create(null); var focus = { createSequence: environment.base.createSequence }; Object.assign(result, tupleStream[0]); for(var ii = 1; ii < tupleStream.length; ii++) { - for(const prop in tupleStream[ii]) { + for(const prop of Object.keys(tupleStream[ii])) { result[prop] = fn.append.call(focus, result[prop], tupleStream[ii][prop]); } } @@ -1333,7 +1333,7 @@ var jsonata = (function() { }; } // merge the update - for(var prop in update) { + for(const prop of Object.keys(update)) { match[prop] = update[prop]; } } @@ -1670,7 +1670,7 @@ var jsonata = (function() { async function applyProcedure(proc, args) { var result; var env = createFrame(proc.environment); - proc.arguments.forEach(function (param, index) { + Array.prototype.forEach.call(proc.arguments, function (param, index) { env.bind(param.value, args[index]); }); if (typeof proc.body === 'function') { @@ -1692,7 +1692,7 @@ var jsonata = (function() { // create a closure, bind the supplied parameters and return a function that takes the remaining (?) parameters var env = createFrame(proc.environment || environment); var unboundArgs = []; - proc.arguments.forEach(function (param, index) { + Array.prototype.forEach.call(proc.arguments, function (param, index) { var arg = args[index]; if (arg && arg.type === 'operator' && arg.value === '?') { unboundArgs.push(param); @@ -1855,14 +1855,14 @@ var jsonata = (function() { * @returns {{bind: bind, lookup: lookup}} Created frame */ function createFrame(enclosingEnvironment) { - var bindings = {}; + var bindings = Object.create(null); const newFrame = { bind: function (name, value) { bindings[name] = value; }, lookup: function (name) { var value; - if(bindings.hasOwnProperty(name)) { + if(Object.prototype.hasOwnProperty.call(bindings, name)) { value = bindings[name]; } else if (enclosingEnvironment) { value = enclosingEnvironment.lookup(name); @@ -2143,7 +2143,7 @@ var jsonata = (function() { var exec_env; // the variable bindings have been passed in - create a frame to hold these exec_env = createFrame(environment); - for (var v in bindings) { + for (const v of Object.keys(bindings)) { exec_env.bind(v, bindings[v]); } } else { diff --git a/src/parser.js b/src/parser.js index 352a1d45..053bdbc6 100644 --- a/src/parser.js +++ b/src/parser.js @@ -351,7 +351,7 @@ const parser = (() => { var node; var lexer; - var symbol_table = {}; + var symbol_table = Object.create(null); var errors = []; var remainingTokens = function () { diff --git a/src/signature.js b/src/signature.js index d0eb4e41..75d289ec 100644 --- a/src/signature.js +++ b/src/signature.js @@ -31,7 +31,7 @@ const signature = (() => { // step through the signature, one symbol at a time var position = 1; var params = []; - var param = {}; + var param = Object.create(null); var prevParam = param; while (position < signature.length) { var symbol = signature.charAt(position); @@ -44,7 +44,7 @@ const signature = (() => { var next = function () { params.push(param); prevParam = param; - param = {}; + param = Object.create(null); }; var findClosingBracket = function (str, start, openSymbol, closeSymbol) { From 2e1a689d7331e69cbfa2d602f611f47b87af7b57 Mon Sep 17 00:00:00 2001 From: Andrew Coleman Date: Mon, 18 May 2026 16:58:41 +0100 Subject: [PATCH 2/2] $append exceeds sequence guardrail The $append function was allowing sequences to be generated that exceeded the sequence length guardrail. Signed-off-by: Andrew Coleman --- src/functions.js | 9 +++++++++ src/jsonata.js | 6 ++++++ test/implementation-tests.js | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/functions.js b/src/functions.js index e1284986..6f1d035a 100644 --- a/src/functions.js +++ b/src/functions.js @@ -1724,6 +1724,15 @@ const functions = (() => { if (!Array.isArray(arg2)) { arg2 = [arg2]; } + const size = arg1.length + arg2.length; + if(this.options && size > this.options.sequence) { + throw { + code: "D2015", + stack: (new Error()).stack, + value: size + }; + } + return arg1.concat(arg2); } diff --git a/src/jsonata.js b/src/jsonata.js index c8057551..90c94569 100644 --- a/src/jsonata.js +++ b/src/jsonata.js @@ -514,6 +514,7 @@ var jsonata = (function() { var result; var focus = { + options: environment.base.options, createSequence: environment.base.createSequence }; switch (expr.value) { @@ -600,6 +601,7 @@ var jsonata = (function() { */ function evaluateWildcard(expr, input, environment) { var focus = { + options: environment.base.options, createSequence: environment.base.createSequence }; var results = focus.createSequence(); @@ -913,10 +915,12 @@ var jsonata = (function() { var groups = Object.create(null); var reduce = input && input.tupleStream ? true : false; var focus = { + options: environment.base.options, createSequence: environment.base.createSequence }; // group the input sequence by 'key' expression if (!Array.isArray(input)) { + options: environment.base.options, input = focus.createSequence(input); } // if the array is empty, add an undefined entry to enable literal JSON object to be generated @@ -995,6 +999,7 @@ var jsonata = (function() { } var result = Object.create(null); var focus = { + options: environment.base.options, createSequence: environment.base.createSequence }; Object.assign(result, tupleStream[0]); @@ -1532,6 +1537,7 @@ var jsonata = (function() { var focus = { environment: environment, input: input, + options: environment.base.options, createSequence: environment.base.createSequence }; // the `focus` is passed in as the `this` for the invoked function diff --git a/test/implementation-tests.js b/test/implementation-tests.js index 102334f0..11070c14 100644 --- a/test/implementation-tests.js +++ b/test/implementation-tests.js @@ -1121,6 +1121,16 @@ describe("Tests that include infinite recursion", () => { code: "D2015", }); }); + + it("prevents appending large sequences", function() { + const options = { + 'sequence': 1000 + } + const expr = jsonata('$append([0..600], [0..600]) ~> $count()', options); + expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({ + code: "D2015", + }); + }); }); });