VDB
EN
CRITICAL 9.9

GHSA-4mr5-g6f9-cfrh

PraisonAI vulnerable to sandbox escape via `print.__self__` builtins module leak in `execute_code` (subprocess mode)

상세

## Summary

`execute_code()` in `praisonaiagents/tools/python_tools.py` (v1.6.37, subprocess sandbox mode) can be fully bypassed using `print.__self__` to retrieve the real Python `builtins` module, from which `__import__` can be extracted via `vars()` and runtime string construction. This achieves arbitrary OS command execution on the host, completely defeating the sandbox.

This is a **novel bypass** that survives all patches for CVE-2026-39888 (frame traversal), CVE-2026-34938 (str subclass), and CVE-2026-40158 (`type.__getattribute__` trampoline).

---

## Severity

**CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H — 9.9 Critical**

---

## Root Cause

Three independent gaps in the AST-based security validation:

### Gap 1: `__self__` missing from `_blocked_attrs`

In CPython, all built-in functions (C-level functions) have a `__self__` attribute that returns the module they belong to. The built-in functions in `safe_builtins` (`print`, `len`, `range`, etc.) are the *real* CPython built-in functions, so `print.__self__` returns `<module 'builtins' (built-in)>`.

The `_blocked_attrs` frozenset (line 52) does NOT include `__self__`. The AST check at line 74 only blocks attributes that are IN this set, so `print.__self__` passes.

### Gap 2: `vars` not blocked as callable or attribute

`builtins.vars(obj)` returns `obj.__dict__`. The function name `vars` is not in the AST `Call` blocklist (line 83: only blocks `exec`, `eval`, `compile`, `__import__`, `open`, `input`, `breakpoint`, `setattr`, `delattr`, `dir`). And `vars` is not in `_blocked_attrs` for attribute access.

So `b.vars(b)` (where `b` is the builtins module) returns `builtins.__dict__` — a dict containing ALL built-in functions including `__import__`, `exec`, `eval`, `open`, etc.

### Gap 3: AST `Call` check only catches `ast.Name` nodes

The dangerous-call check (line 82-88) only fires when `isinstance(func, ast.Name)` — i.e., bare-name calls like `exec(...)`. It does NOT catch: - Attribute calls: `b.exec(...)` — func is `ast.Attribute` - Subscript calls: `d["exec"](...)` — func is `ast.Subscript`

### Gap 4: Runtime string construction bypasses string constant check

The string constant check (line 92-98) catches literals like `"__import__"`, but NOT runtime concatenation like `"_" + "_" + "import" + "_" + "_"`. The AST sees 5 separate `Constant` nodes (`"_"`, `"_"`, `"import"`, `"_"`, `"_"`), none of which contain any blocked attr as a substring.

---

## Proof of Concept

```python from praisonaiagents.tools.python_tools import execute_code

# Exploit: 4 lines, bypasses ALL security layers payload = """ b = print.__self__ d = b.vars(b) key = "_" + "_" + "import" + "_" + "_" imp = d[key] mod = imp("os") print(mod.popen("id").read()) """

result = execute_code(code=payload) print(result) # Output: {'result': None, 'stdout': 'uid=1000(user) gid=1000(user) ...\n', 'stderr': '', 'success': True} ```

### Step-by-step bypass analysis:

| Line | AST node | Check | Result | |---|---|---|---| | `print.__self__` | `Attribute(attr='__self__')` | `__self__` in `_blocked_attrs`? | **NO** → passes | | `b.vars` | `Attribute(attr='vars')` | `vars` in `_blocked_attrs`? | **NO** → passes | | `b.vars(b)` | `Call(func=Attribute)` | `isinstance(func, ast.Name)`? | **NO** → passes | | `"_"`, `"import"` | `Constant(value=str)` | Contains blocked attr? | **NO** → passes | | `d[key]` | `Subscript` | Not checked | passes | | `imp("os")` | `Call(func=Name('imp'))` | `imp` in blocked calls? | **NO** → passes |

**Result: Full sandbox escape → arbitrary command execution**

---

## Impact

An attacker who can influence agent input (via prompt injection, malicious documents, or direct code submission) can:

- Execute arbitrary commands on the host system - Read/write any file accessible to the process - Exfiltrate environment variables, API keys, and credentials - Pivot to internal networks - Install persistent backdoors

---

## Affected

- **Package**: `praisonaiagents` (PyPI) - **Affected versions**: All versions through 1.6.37 (latest) - **Component**: `praisonaiagents/tools/python_tools.py`, `_execute_code_sandboxed()` function - **Default configuration affected**: Yes (`sandbox_mode="sandbox"` is the default)

---

## Remediation

### Immediate fix Add `__self__` to `_blocked_attrs`: ```python _blocked_attrs = frozenset({ ..., '__self__', # Built-in functions leak their parent module }) ```

### Additional hardening 1. Block `vars` in the callable blocklist 2. Extend the `ast.Call` check to also catch `ast.Attribute` and `ast.Subscript` function nodes 3. Add AST check for `BinOp` string concatenation that could construct blocked attr names

### Fundamental recommendation Denylist-based Python sandboxes are fundamentally insecure. Each patch introduces a new bypass opportunity. Consider: - Using `isolated-vm` (Node.js) or WebAssembly-based isolation - Using OS-level sandboxing (seccomp, namespaces, gVisor) - Removing in-process code execution entirely in favor of containerized execution

이 버전이 영향받나요?

사용 중인 패키지 버전을 입력하면 즉시 평가합니다.

영향 패키지

PyPI / praisonaiagents
최초 영향 버전: 0 수정 버전: 1.6.40
수정 pip install --upgrade 'praisonaiagents>=1.6.40'
PyPI / praisonai
최초 영향 버전: 0 수정 버전: 4.6.40
수정 pip install --upgrade 'praisonai>=4.6.40'

참고