PT-2026-53145 · Pypi · Praisonaiagents

Published

2026-06-18

·

Updated

2026-06-18

CVSS v3.1

6.5

Medium

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

Summary

The execute code tool's subprocess sandbox advertises a three-layer defense (AST validation, text-pattern blocklist, restricted builtins). In sandbox mode (the default) only two layers are active — the text-pattern blocklist is skipped — and both remaining layers are bypassed by combining two CPython semantics:
  1. Runtime string assembly. The AST validator (src/praisonai-agents/praisonaiagents/tools/python tools.py:75) enumerates blocked dunder names against ast.Attribute.attr, ast.Call.func.id, and ast.Constant string-substring. Names assembled at runtime (e.g. " "*2 + "class" + " "*2) appear in the AST as multiple short ast.Constant nodes, none containing a blocked substring, so the static check passes.
  2. C-level attribute access via format-spec. str.format / str.format map resolve dotted field references through CPython's internal PyObject GetAttr (do string formatget field). This C path never consults the Python-level getattr binding. The sandbox's safe getattr wrapper (python tools.py:221) is installed only as the getattr name in safe builtins, so any C-level attribute access — including format-spec field resolution — sidesteps it. format/format map are also absent from SANDBOX BLOCKED CALLS (python tools.py:56).
Combined, this yields an arbitrary read primitive over every blocklisted attribute (class, qualname, bases, base, function globals, dict, …).

Affected code

FileLinesSymbolRole
src/praisonai-agents/praisonaiagents/tools/python tools.py39–54 SANDBOX BLOCKED ATTRSThe blocklist the bypass renders unreachable
src/praisonai-agents/praisonaiagents/tools/python tools.py56–60 SANDBOX BLOCKED CALLSMissing entries: format, format map
src/praisonai-agents/praisonaiagents/tools/python tools.py75–102 validate code astStatic check, blind to runtime string assembly
src/praisonai-agents/praisonaiagents/tools/python tools.py221–226 safe getattrWraps Python-level getattr only; C-level access bypasses
src/praisonai-agents/praisonaiagents/tools/python tools.py352execute codeEntry point; gated by @require approval(risk level="critical")

Reproducer

python
import os
os.environ["PRAISONAI AUTO APPROVE"] = "true"
from praisonaiagents.tools.python tools import execute code

payload = '''
und = " " * 2             # " " assembled at runtime
key1 = und + "class" + und      # " class "
key2 = und + "qualname" + und     # " qualname "
fmt class = "{0." + key1 + "}"
fmt qual2 = "{0." + key1 + "." + key2 + "}"
print("LEAK CLASS=" + fmt class.format(()))
print("LEAK QUAL2=" + fmt qual2.format(()))
'''
print(execute code(payload, sandbox mode="sandbox", timeout=15))
Observed: stdout = LEAK CLASS=<class 'tuple'> / LEAK QUAL2=tuple, success: true, no security error. Both class (one hop) and class . qualname (two hops) — all blocklisted — are read.

Trust boundary

The @require approval(risk level="critical") gate is bypassed when PRAISONAI AUTO APPROVE is set (verified: require approval short-circuits on is env auto approve()). That variable is set by the project's FULL AUTO autonomy mode, the bots-CLI launchers, and the project's own issue-triage CI workflow — postures where the agent reaches execute code with no human approval. The payload then arrives via any LLM-visible surface (user message, retrieved document, tool/web/MCP output) and the tool-call machinery passes it as the code argument.

Relationship to GHSA-4mr5-g6f9-cfrh

The code's own comment at python tools.py:46 cites GHSA-4mr5-g6f9-cfrh, which added self to the blocklist to stop C-builtins leaking builtins via func. self . This finding does not bypass that single entry — it bypasses the entire blocklist, because format-spec attribute resolution never consults the blocklist or safe getattr. "{0. self }".format(print) would leak self regardless of the blocklist. Same defense surface, different mechanism; the GHSA-4mr5 fix does not mitigate this.

Scope (read primitive only)

This reports the read primitive. Turning the read into in-process execution requires a callable bridge; the obvious one (string.Formatter().get field() returning the live object) is not directly reachable because import string is blocked at the AST layer (no ast.Import). Other bridges may exist; a full execution chain is not claimed here. If one is found, severity rises to ~8.8 (the subprocess has no seccomp/setrlimit/syscall filtering).

Suggested fix

  1. Add format, format map to SANDBOX BLOCKED CALLS (blocks the calls at the AST layer; cost: also blocks benign str.format).
  2. Or replace str in safe builtins with a subclass whose format/format map reject dotted fields resolving to leading-underscore attributes (preserves benign formatting).
  3. Or drop sandbox-mode's in-process security claim and document that real isolation requires external sandboxing (gVisor/firejail/container/microVM) — which matches what the subprocess provides today.
The text-pattern blocklist present in the direct path (python tools.py:487-502) is absent from the sandbox path; even if added, the runtime-assembly trick defeats it, so (1) or (2) is required.
Reporter: Kai Aizen / SnailSploit — kai@snailsploit.com — PGP on request. Coordinated disclosure; no public posting.

Fix

Protection Mechanism Failure

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

Weakness Enumeration

Related Identifiers

GHSA-PV2J-RGHR-V5R9

Affected Products

Praisonaiagents