PT-2026-53145 · Pypi · Praisonaiagents
Published
2026-06-18
·
Updated
2026-06-18
CVSS v3.1
6.5
Medium
| Vector | AV: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:- Runtime string assembly. The AST validator (
src/praisonai-agents/praisonaiagents/tools/python tools.py:75) enumerates blocked dunder names againstast.Attribute.attr,ast.Call.func.id, andast.Constantstring-substring. Names assembled at runtime (e.g." "*2 + "class" + " "*2) appear in the AST as multiple shortast.Constantnodes, none containing a blocked substring, so the static check passes. - C-level attribute access via format-spec.
str.format/str.format mapresolve dotted field references through CPython's internalPyObject GetAttr(do string format→get field). This C path never consults the Python-levelgetattrbinding. The sandbox'ssafe getattrwrapper (python tools.py:221) is installed only as thegetattrname insafe builtins, so any C-level attribute access — including format-spec field resolution — sidesteps it.format/format mapare also absent fromSANDBOX 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
| File | Lines | Symbol | Role |
|---|---|---|---|
src/praisonai-agents/praisonaiagents/tools/python tools.py | 39–54 | SANDBOX BLOCKED ATTRS | The blocklist the bypass renders unreachable |
src/praisonai-agents/praisonaiagents/tools/python tools.py | 56–60 | SANDBOX BLOCKED CALLS | Missing entries: format, format map |
src/praisonai-agents/praisonaiagents/tools/python tools.py | 75–102 | validate code ast | Static check, blind to runtime string assembly |
src/praisonai-agents/praisonaiagents/tools/python tools.py | 221–226 | safe getattr | Wraps Python-level getattr only; C-level access bypasses |
src/praisonai-agents/praisonaiagents/tools/python tools.py | 352 | execute code | Entry 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
- Add
format,format maptoSANDBOX BLOCKED CALLS(blocks the calls at the AST layer; cost: also blocks benignstr.format). - Or replace
strinsafe builtinswith a subclass whoseformat/format mapreject dotted fields resolving to leading-underscore attributes (preserves benign formatting). - 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
Affected Products
Praisonaiagents