CVE-2026-57120
mediumpriority · track · 0EPSS 0.006 · 46 %ileUSEPSS · scored 2026-10-27
PraisonAI is a multi-agent teams system. Prior to praisonaiagents 1.6.59, execute_code sandbox mode permits runtime assembly of blocklisted dunder names and allows str.format or str.format_map to resolve dotted fields through C-level attribute access that bypasses _safe_getattr. This exposes class, qualified-name, base-class, globals, and object-dictionary attributes to prompt-influenced code when approval is automatically granted, producing a high-impact read primitive without establishing a complete in-process execution chain. This issue is fixed in praisonaiagents 1.6.59.
cvss
Technical severity, 0–10. One row per scoring source.
base score 0–10 · higher = more severe · hover a vector to decode
ssvc
A stakeholder decision — act, attend, or track — from exploitation and impact.
weaknesses
The CWE weakness classes this flaw belongs to.
affected products (1)
Products and version ranges known to be affected.
advisories (2)
Vendor and ecosystem advisories describing this.
- GHSA-pv2j-rghr-v5r9PraisonAI: execute_code sandbox bypass: str.format C-level attribute access reads every blocklisted dunder2026-06-18ghsa · osvUS
## 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_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__`, …). ## 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 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.
- PYSEC-2026-3533PraisonAI: execute_code sandbox bypass: str.format C-level attribute access reads every blocklisted dunder2026-07-23vendor · osvUS
## 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_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__`, …). ## 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 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.
also known as (2)
Other names and identifiers for this entity.
sources
Which sources assert this record, and when.
references (3) nvd
External citations — patches, advisories, analysis.
Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.
Free scan