GHSA-pv2j-rghr-v5r9 CVE-2026-57120 PYSEC-2026-3533
PraisonAI: execute_code sandbox bypass: str.format C-level attribute access reads every blocklisted dunder
- Severity
- 6.5 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.56%
- Chance of exploitation in the next 30 days. Higher than 46.30% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-693 Protection Mechanism Failure
- Fix
1.6.59- Upgrade praisonaiagents to this version or later
Description
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's_safe_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 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
| 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
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_mapto_SANDBOX_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.
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| praisonaiagents | < 1.6.59 |
1.6.59 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
praisonaiagents PyPI → 1.6.59
- pip
pip install --upgrade "praisonaiagents>=1.6.59"- poetry
poetry add "praisonaiagents>=1.6.59"- uv
uv add "praisonaiagents>=1.6.59"
Severity breakdown
CVSS 3.1 6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Weakness
CWE-693: Protection Mechanism Failure
The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-gxmw-5f7x-6g22 praisonaiagents vulnerable to arbitrary file write via unsanitized `user_id` in `FileMemory.__init__()` — path traversal to any writable location | High | Also affects praisonaiagents |
| GHSA-cfxv-8fw8-rwpv praisonaiagents: ast_grep_rewrite rewrites arbitrary files without the @require_approval gate enforced on every sibling mutation tool | Medium | Also affects praisonaiagents |
| GHSA-x44h-65qv-cw74 praisonaiagents has an SSRF protection bypass in `spider_tools._host_is_blocked()` via DNS-resolved hostnames (`127.0.0.1.nip.io`) | High | Also affects praisonaiagents |
| GHSA-7g3p-92qq-8wvh praisonaiagents: AgentServer declares auth_token but never enforces it on any route | High | Also affects praisonaiagents |
| GHSA-5r34-2g38-6569 praisonaiagents web_crawl vulnerable to SSRF via redirect-following | High | Also affects praisonaiagents |
| GHSA-vg6p-v9vm-6fgj praisonaiagents vulnerable to SSRF in web_crawl tool via redirect-following and DNS rebinding (validate-then-fetch gap) | High | Also affects praisonaiagents |
| GHSA-8hjw-25cg-g52h praisonaiagents has a `web_crawl` SSRF protection bypass via unchecked redirect targets | High | Also affects praisonaiagents |
| GHSA-hxmv-c4g6-5fqc PraisonAI workflow include bypasses tools.py autoload opt-in and executes included recipe code | High | Also affects praisonaiagents |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor