GHSA-pv2j-rghr-v5r9
MEDIUMGHSA-pv2j-rghr-v5r9 is a medium-severity (CVSS 6.5) CWE-693 vulnerability in praisonaiagents. O3 Security confirms whether GHSA-pv2j-rghr-v5r9 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
PraisonAI: execute_code sandbox bypass: str.format C-level attribute access reads every blocklisted dunder
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-pv2j-rghr-v5r9.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-pv2j-rghr-v5r9 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 374,847 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
praisonaiagentsReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
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 — [email protected] — PGP on request. Coordinated disclosure; no public posting.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | praisonaiagents | all versions | 1.6.59 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for praisonaiagents. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update praisonaiagents to 1.6.59 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-pv2j-rghr-v5r9 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-pv2j-rghr-v5r9 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-pv2j-rghr-v5r9. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-pv2j-rghr-v5r9 in your dependencies?
O3 detects GHSA-pv2j-rghr-v5r9 across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.