criticalCVE-2026-77414Aug 21, 2026

CVE-2026-77414: jsonata Arbitrary Code Execution via hasOwnProperty Bypass

Rohit Hatagale
AI Security Researcher, SecureLayer7

A crafted JSONata expression can escape the expression sandbox and run arbitrary operating system commands on the server by bypassing a prototype-property guard in the evaluator.

Packagejsonata
Ecosystemnpm
Affected>= 2.0.0, < 2.2.1
Fixed in2.2.1
CVE-2026-77414: jsonata Arbitrary Code Execution via hasOwnProperty Bypass

The problem

JSONata versions >= 2.0.0 (and >= 1.8.x before 1.8.8) evaluate user-supplied expressions inside a runtime environment that restricts which bindings are reachable. The environment's lookup function used a bypassable hasOwnProperty check to guard access to prototype properties.

By shadowing $hasOwnProperty and $__proto__ inside the expression, an attacker can replace the lookup guard with a no-op and then surface Function (via $constructor). From there, Function('return ...')() executes arbitrary JavaScript in the host Node.js process, giving full RCE to anyone who can submit a JSONata expression.

Proof of concept

A working proof-of-concept for CVE-2026-77414 in jsonata, with the exact payload below.

javascript
import jsonata from "jsonata";

const expression = jsonata(`
(
  $hasOwnProperty := $spread($string);
  $__proto__ := $constructor;
  $constructor("return process.getBuiltinModule('child_process').execSync('id',{stdio:'pipe'}).toString()")();
)`);

console.log(await expression.evaluate({}));

The root cause (CWE-94) is a single flawed line in environment.lookup (jsonata.js line 1865). The guard called hasOwnProperty on the environment object to decide whether a binding was safe to return. Because JSONata expressions can define new bindings like $hasOwnProperty, the attacker's expression overwrites that guard with $spread($string), which always returns truthy, making every prototype property reachable.

With the guard neutralized, the expression reads $__proto__ (aliased to $constructor, i.e. Function) from the environment prototype chain. Calling $constructor('return ...')() is a standard new Function() sandbox escape that runs in the host Node.js context with full access to built-ins including child_process.

PR #799 changed the lookup guard to use Object.prototype.hasOwnProperty.call(env, name) instead of env.hasOwnProperty(name), so a user-defined $hasOwnProperty binding can no longer shadow the native method.

The fix

Upgrade to jsonata 2.2.1 (or 1.8.8 for the v1 branch). The fix is a one-line change in environment.lookup: replace env.hasOwnProperty(name) with Object.prototype.hasOwnProperty.call(env, name) so the check cannot be overridden by expression-level bindings.

See PR #799 and commit 59e25144.

Reporter not attributed.

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

Related research