A Function-constructor escape we fixed in JSONPath-Plus

We reported and fixed a remote code execution escape in JSONPath-Plus, a library pulled 12 million times a week: its “safe” eval still handed back the Function constructor through a property read, so obj.constructor reached Function and ran arbitrary code. The fix, publicly credited to Ryan Cruz in PR #266, refuses to return Function.

Diagram showing the jsonpath-plus safe eval blocked the Function constructor in its call-expression path but returned any function from a property read in its member-expression path, so obj.constructor resolved to Function and gave remote code execution

On this page: the bug · what safe eval is · the escape · the fix · the CVE chain · disclosure · what it means for testing

The bug in one sentence

JSONPath-Plus lets callers evaluate path expressions through an eval mode that is meant to be sandboxed. The sandbox blocked the Function constructor in one code path and not another, so a member expression like obj.constructor returned Function itself, and everything downstream of the Function constructor is arbitrary code.

FieldValue
Packagejsonpath-plus (npm)
Reach~12.4 million downloads a week
ClassSandbox escape to the Function constructor (RCE)
Root causemember-expression path returned Function unchecked
FixPR #266, one added clause, merged July 2, 2026
CreditPublicly “Reported by: Ryan Cruz”

What safe eval was supposed to do

JSONPath-Plus is a widely used implementation of JSONPath, the query language for JSON, at roughly 12.4 million downloads a week. Some expressions need to evaluate JavaScript-like sub-expressions, and for that the library ships a guarded evaluator that is supposed to allow the useful parts of expression evaluation while denying the dangerous ones.

The dangerous one, always, is the Function constructor. In JavaScript, Function("...body...") compiles a string into a callable with access to the global scope. Any sandbox that lets an attacker reach Function is not a sandbox. So the library kept a denylist of blocked properties that included constructor, and checked for the Function constructor before calling a resolved function.

The check existed, in one place

The call-expression handler did the right thing. As the fixed code shows, it knew Function was the thing to stop:

if (func === Function) { // unreachable since BLOCKED_PROTO_PROPERTIES includes ‘constructor’

The comment even asserts the case is unreachable. It was not, because the guard lived on the calling path, not on the path that produced the value.

The escape: a property read that returns Function

Here is the gap. When the evaluator resolved a member expression, it returned any value that happened to be a function, and bound it, with no check for which function:

// vulnerable: returns ANY function, including Function itself
if (typeof result === 'function') {
  return result.bind(obj);
}

From a property read to a shell

Reading obj.constructor resolves to Function. The member-expression handler saw a function and handed it back. Now the attacker holds a reference to the Function constructor through a path the call-expression guard never saw, and the “unreachable” branch was reachable after all. From that reference, Function(payload)() is arbitrary code execution.

This is the same primitive we found in a different library: reaching the Function constructor through a property read that a write-path guard did not cover. Two libraries, same class of bug, and it is worth internalizing why.

The fix is one clause

The patch states the missing invariant directly: return the function only when it is not the Function constructor.

// before
if (typeof result === 'function') {
  return result.bind(obj);
}

// after: never hand back Function itself
if (typeof result === 'function' && result !== Function) {
  return result.bind(obj);
}

Merged in PR #266 with new tests, moving the suite from 278 to 280 cases. The one-clause shape is typical of these bugs: the logic was almost right, and the gap was a value that one path checked and another did not.

The same escape hatch, patched three times

The most useful part of this finding is the history, because it shows how a sandbox erodes. JSONPath-Plus has closed a route to the Function constructor more than once.

Timeline of the jsonpath-plus safe eval RCE chain: the original eval RCE CVE-2024-21534, an incomplete fix leading to CVE-2025-1302 patched in version 10.3.0, and the newest member-path bypass reported by Ryan Cruz in pull request 266

StageWhat it was
CVE-2024-21534the original eval RCE
CVE-2025-1302an incomplete fix, RCE again, patched in 10.3.0, rated 9.8
PR #266the member-expression path still returned Function

CVE-2025-1302 was rated 9.8 on the CVSS scale and described by its advisory as caused by an incomplete fix for the one before it. Our finding is the next in that line: not the same bug, the same escape hatch through a door the previous fixes did not close.

How we disclosed it

Through GitHub, on the project’s own terms. We reported the escape and wrote the fix in a pull request, which is public and credited to Ryan Cruz, merged by the maintainer on July 2, 2026. Coordinated disclosure on open source is straightforward: propose the fix, let the maintainer review and merge, and the credit trail is on the record. That record is the point.

The pattern to take away

A denylist that blocks a dangerous value in one code path but not in every code path is not a fix. It is a smaller target. The Function constructor was blocked where functions were called and not where functions were read, and the sandbox held right up until someone read instead of called. Finding that kind of gap is a reading problem, which is what a real penetration test does that a scanner, matching patterns, does not.

What a library RCE says about testing

We publish findings like this, and the critical RCE in velocity.js alongside it, because depth is the one thing a security vendor cannot fake. Anyone can say their testing is deep. A fixed, credited RCE in a library with millions of weekly installs is a claim you can click on. That same white-box reading, pointed at your application every month, is what our subscription buys, and our benchmark runs are public for the parts a disclosure cannot show.

Hackers stay in the loop

None of this is the model alone. AI surfaces the suspicious property walk; a human confirms it reaches Function, writes the working proof, and files the pull request the maintainer can review and merge. That division of labor is the whole point: coverage no human team can afford monthly, plus a person who verifies the finding is real before it goes anywhere.

The pricing is public too: $299 a month for early-stage startups, $2,999 for everyone else, against the $5,000 to $30,000 a single traditional test costs. A startup’s year of continuous testing plus an independent SOC 2 attestation lands near $6,088, versus the $30,000-plus a three-vendor stack runs.

The short version

  • We reported and fixed a Function-constructor escape in JSONPath-Plus, a library pulled about 12 million times a week.
  • The safe eval blocked Function on the call path but not the member path, so obj.constructor returned Function and gave RCE.
  • The fix, in PR #266 credited to Ryan Cruz, returns a function only when it is not Function.
  • It is the newest link in the library’s safe-eval chain, after CVE-2024-21534 and CVE-2025-1302.
  • Public, credited findings are the check you can run on any tester’s claim of depth. Ours is a subscription, and the engine points at your stack every month.

Frequently asked questions

Is my application affected?

You are at risk if you evaluate JSONPath expressions that an attacker can influence, on a version of jsonpath-plus whose safe eval still returns the Function constructor through a member expression. The safest posture is to upgrade to the latest release once it carries the PR #266 fix, and to never pass attacker-controlled JSONPath strings to the eval-backed paths. If your expressions are all static and developer-authored, the exposure is far lower, but the eval surface is worth removing regardless.

What actually goes wrong here?

The library's safe eval blocked the Function constructor in its call-expression handling but not in its member-expression handling. Reading a property that resolves to a function returned that function directly, and obj.constructor resolves to Function. From the Function constructor, an attacker compiles and runs arbitrary code, which is full remote code execution. The fix refuses to return a function when it is the Function constructor itself.

How is this different from the earlier jsonpath-plus CVEs?

It is the same escape hatch, narrowed again. CVE-2024-21534 was the original eval RCE, and CVE-2025-1302 was an incomplete fix that reintroduced it, patched in 10.3.0. Each fix closed one route to the Function constructor while another remained. The pattern is the lesson: a denylist that blocks a dangerous value in one code path but not every code path is not a fix, it is a smaller target.

How was this disclosed?

Through the project's own process on GitHub. We reported the escape and authored the fix in pull request 266, which is publicly credited Reported by: Ryan Cruz and was merged on July 2, 2026. Coordinated disclosure on open-source means the maintainer gets the fix and the credit trail is public, which is exactly why a finding like this is a claim you can verify rather than take on faith.

How does HackZero find bugs like this and still charge $299 a month?

We own the whole testing stack, so the marginal cost of a run is compute plus senior review, not tester-weeks. The attack agents, the exploitation tooling, and the reporting are all built in-house: AI does the continuous coverage no human team can afford monthly, and hackers stay in the loop to confirm a finding is real, reduce it, and report it responsibly. Owning the stack is also why one subscription can fold in SOC 2 controls and connect you with an independent AICPA-member CPA who attests them. Startups pay $299 a month, every other company $2,999.