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:
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.str.format / str.format_map resolve dotted field references through CPython's internal PyObject_GetAttr (do_string_format → get_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__, …).
| 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") |
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.
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.
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.
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).
format, format_map to _SANDBOX_BLOCKED_CALLS (blocks the calls at the AST layer; cost: also blocks benign str.format).str in safe_builtins with a subclass whose format/format_map reject dotted fields resolving to leading-underscore attributes (preserves benign formatting).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 — [email protected] — PGP on request. Coordinated disclosure; no public posting.
| Software | From | Fixed in |
|---|---|---|
praisonaiagents
|
- | 1.6.59 |
A security vulnerability is a weakness in software, hardware, or configuration that can be exploited to compromise confidentiality, integrity, or availability. Many vulnerabilities are tracked as CVEs (Common Vulnerabilities and Exposures), which provide a standardized identifier so teams can coordinate patching, mitigation, and risk assessment across tools and vendors.
CVSS (Common Vulnerability Scoring System) estimates technical severity, but it doesn't automatically equal business risk. Prioritize using context like internet exposure, affected asset criticality, known exploitation (proof-of-concept or in-the-wild), and whether compensating controls exist. A "Medium" CVSS on an exposed, production system can be more urgent than a "Critical" on an isolated, non-production host.
A vulnerability is the underlying weakness. An exploit is the method or code used to take advantage of it. A zero-day is a vulnerability that is unknown to the vendor or has no publicly available fix when attackers begin using it. In practice, risk increases sharply when exploitation becomes reliable or widespread.
Recurring findings usually come from incomplete Asset Discovery, inconsistent patch management, inherited images, and configuration drift. In modern environments, you also need to watch the software supply chain: dependencies, containers, build pipelines, and third-party services can reintroduce the same weakness even after you patch a single host. Unknown or unmanaged assets (often called Shadow IT) are a common reason the same issues resurface.
Use a simple, repeatable triage model: focus first on externally exposed assets, high-value systems (identity, VPN, email, production), vulnerabilities with known exploits, and issues that enable remote code execution or privilege escalation. Then enforce patch SLAs and track progress using consistent metrics so remediation is steady, not reactive.
SynScan combines attack surface monitoring and continuous security auditing to keep your inventory current, flag high-impact vulnerabilities early, and help you turn raw findings into a practical remediation plan.