{"id":"PYSEC-2026-3533","summary":"PraisonAI: execute_code sandbox bypass: str.format C-level attribute access reads every blocklisted dunder","details":"## Summary\n\nThe `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:\n\n1. **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.\n2. **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`).\n\nCombined, this yields an arbitrary read primitive over every blocklisted attribute (`__class__`, `__qualname__`, `__bases__`, `__base__`, function `__globals__`, `__dict__`, …).\n\n## Affected code\n\n| File | Lines | Symbol | Role |\n|---|---|---|---|\n| `src/praisonai-agents/praisonaiagents/tools/python_tools.py` | 39–54 | `_SANDBOX_BLOCKED_ATTRS` | The blocklist the bypass renders unreachable |\n| `src/praisonai-agents/praisonaiagents/tools/python_tools.py` | 56–60 | `_SANDBOX_BLOCKED_CALLS` | Missing entries: `format`, `format_map` |\n| `src/praisonai-agents/praisonaiagents/tools/python_tools.py` | 75–102 | `_validate_code_ast` | Static check, blind to runtime string assembly |\n| `src/praisonai-agents/praisonaiagents/tools/python_tools.py` | 221–226 | `_safe_getattr` | Wraps Python-level `getattr` only; C-level access bypasses |\n| `src/praisonai-agents/praisonaiagents/tools/python_tools.py` | 352 | `execute_code` | Entry point; gated by `@require_approval(risk_level=\"critical\")` |\n\n## Reproducer\n\n```python\nimport os\nos.environ[\"PRAISONAI_AUTO_APPROVE\"] = \"true\"\nfrom praisonaiagents.tools.python_tools import execute_code\n\npayload = '''\nund = \"_\" * 2                         # \"__\" assembled at runtime\nkey1 = und + \"class\" + und            # \"__class__\"\nkey2 = und + \"qualname\" + und         # \"__qualname__\"\nfmt_class = \"{0.\" + key1 + \"}\"\nfmt_qual2 = \"{0.\" + key1 + \".\" + key2 + \"}\"\nprint(\"LEAK_CLASS=\" + fmt_class.format(()))\nprint(\"LEAK_QUAL2=\" + fmt_qual2.format(()))\n'''\nprint(execute_code(payload, sandbox_mode=\"sandbox\", timeout=15))\n```\n\nObserved: `stdout` = `LEAK_CLASS=\u003cclass 'tuple'\u003e` / `LEAK_QUAL2=tuple`, `success: true`, no security error. Both `__class__` (one hop) and `__class__.__qualname__` (two hops) — all blocklisted — are read.\n\n## Trust boundary\n\nThe `@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.\n\n## Relationship to GHSA-4mr5-g6f9-cfrh\n\nThe 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.\n\n## Scope (read primitive only)\n\nThis 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).\n\n## Suggested fix\n\n1. Add `format`, `format_map` to `_SANDBOX_BLOCKED_CALLS` (blocks the calls at the AST layer; cost: also blocks benign `str.format`).\n2. Or replace `str` in `safe_builtins` with a subclass whose `format`/`format_map` reject dotted fields resolving to leading-underscore attributes (preserves benign formatting).\n3. 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.\n\nThe 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.\n\nReporter: Kai Aizen / SnailSploit — kai@snailsploit.com — PGP on request. Coordinated disclosure; no public posting.","aliases":["CVE-2026-57120","GHSA-pv2j-rghr-v5r9"],"modified":"2026-07-23T15:00:23.528381836Z","published":"2026-07-23T11:41:41.087045Z","references":[{"type":"WEB","url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-pv2j-rghr-v5r9"},{"type":"PACKAGE","url":"https://github.com/MervinPraison/PraisonAI"},{"type":"PACKAGE","url":"https://pypi.org/project/praisonaiagents"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-pv2j-rghr-v5r9"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-57120"}],"affected":[{"package":{"name":"praisonaiagents","ecosystem":"PyPI","purl":"pkg:pypi/praisonaiagents"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.6.59"}]}],"versions":["0.0.1","0.0.10","0.0.100","0.0.101","0.0.102","0.0.103","0.0.104","0.0.105","0.0.106","0.0.107","0.0.108","0.0.109","0.0.11","0.0.110","0.0.111","0.0.112","0.0.113","0.0.114","0.0.115","0.0.116","0.0.117","0.0.118","0.0.119","0.0.12","0.0.120","0.0.121","0.0.122","0.0.123","0.0.124","0.0.125","0.0.126","0.0.127","0.0.128","0.0.129","0.0.13","0.0.130","0.0.131","0.0.132","0.0.133","0.0.134","0.0.135","0.0.136","0.0.137","0.0.138","0.0.139","0.0.14","0.0.140","0.0.141","0.0.142","0.0.143","0.0.144","0.0.145","0.0.146","0.0.147","0.0.148","0.0.149","0.0.15","0.0.150","0.0.151","0.0.152","0.0.153","0.0.154","0.0.155","0.0.156","0.0.157","0.0.158","0.0.159","0.0.16","0.0.160","0.0.161","0.0.162","0.0.163","0.0.164","0.0.165","0.0.166","0.0.167","0.0.168","0.0.169","0.0.17","0.0.170","0.0.171","0.0.172","0.0.173","0.0.174","0.0.175","0.0.176","0.0.177","0.0.178","0.0.179","0.0.18","0.0.180","0.0.181","0.0.182","0.0.183","0.0.184","0.0.185","0.0.187","0.0.188","0.0.189","0.0.19","0.0.190","0.0.191","0.0.192","0.0.193","0.0.194","0.0.195","0.0.196","0.0.197","0.0.198","0.0.199","0.0.2","0.0.20","0.0.21","0.0.22","0.0.23","0.0.24","0.0.25","0.0.26","0.0.27","0.0.28","0.0.29","0.0.3","0.0.30","0.0.31","0.0.32","0.0.33","0.0.34","0.0.35","0.0.36","0.0.37","0.0.38","0.0.39","0.0.4","0.0.40","0.0.41","0.0.42","0.0.43","0.0.44","0.0.45","0.0.46","0.0.47","0.0.48","0.0.49","0.0.5","0.0.50","0.0.51","0.0.52","0.0.53","0.0.54","0.0.56","0.0.57","0.0.58","0.0.59","0.0.6","0.0.60","0.0.61","0.0.62","0.0.63","0.0.64","0.0.65","0.0.66","0.0.67","0.0.68","0.0.69","0.0.7","0.0.70","0.0.71","0.0.72","0.0.73","0.0.74","0.0.75","0.0.76","0.0.77","0.0.78","0.0.79","0.0.8","0.0.80","0.0.81","0.0.82","0.0.83","0.0.84","0.0.85","0.0.86","0.0.87","0.0.88","0.0.89","0.0.9","0.0.90","0.0.91","0.0.92","0.0.93","0.0.94","0.0.95","0.0.96","0.0.97","0.0.98","0.0.99","0.1.0","0.1.1","0.1.10","0.1.11","0.1.12","0.1.13","0.1.14","0.1.15","0.1.16","0.1.17","0.1.18","0.1.19","0.1.2","0.1.20","0.1.21","0.1.22","0.1.23","0.1.24","0.1.25","0.1.26","0.1.27","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.1.8","0.1.9","0.10.0","0.10.1","0.10.10","0.10.2","0.10.3","0.10.4","0.10.5","0.10.6","0.10.7","0.10.8","0.10.9","0.11.0","0.11.1","0.11.10","0.11.11","0.11.12","0.11.13","0.11.14","0.11.15","0.11.16","0.11.17","0.11.18","0.11.19","0.11.2","0.11.20","0.11.21","0.11.22","0.11.23","0.11.24","0.11.25","0.11.27","0.11.28","0.11.29","0.11.3","0.11.30","0.11.31","0.11.4","0.11.5","0.11.6","0.11.7","0.11.8","0.11.9","0.12.0","0.12.1","0.12.10","0.12.11","0.12.12","0.12.13","0.12.14","0.12.15","0.12.16","0.12.17","0.12.18","0.12.19","0.12.2","0.12.20","0.12.21","0.12.3","0.12.4","0.12.5","0.12.6","0.12.7","0.12.8","0.12.9","0.13.0","0.13.1","0.13.10","0.13.11","0.13.12","0.13.13","0.13.14","0.13.15","0.13.16","0.13.17","0.13.18","0.13.19","0.13.2","0.13.20","0.13.21","0.13.22","0.13.23","0.13.3","0.13.4","0.13.5","0.13.6","0.13.7","0.13.8","0.13.9","0.14.0","0.14.1","0.14.10","0.14.11","0.14.12","0.14.14","0.14.15","0.14.16","0.14.2","0.14.3","0.14.4","0.14.5","0.14.6","0.14.7","0.14.8","0.14.9","0.15.0","0.15.1","0.15.2","0.15.3","0.2.0","0.2.1","0.2.2","0.3.0","0.3.1","0.3.2","0.3.3","0.3.4","0.4.0","0.4.1","0.5.0","0.5.1","0.5.2","0.5.3","0.6.0","0.6.1","0.6.2","0.6.3","0.6.4","0.6.5","0.6.6","0.6.7","0.6.8","0.7.0","0.7.1","0.8.0","0.8.1","0.9.0","0.9.1","1.0.0","1.1.0","1.2.0","1.2.1","1.2.2","1.2.3","1.2.4","1.3.0","1.3.1","1.4.0","1.4.1","1.4.2","1.4.3","1.4.4","1.4.5","1.4.6","1.4.7","1.4.8","1.5.0","1.5.1","1.5.10","1.5.100","1.5.101","1.5.102","1.5.103","1.5.104","1.5.105","1.5.106","1.5.107","1.5.108","1.5.109","1.5.11","1.5.110","1.5.111","1.5.112","1.5.113","1.5.114","1.5.115","1.5.116","1.5.117","1.5.118","1.5.119","1.5.12","1.5.120","1.5.121","1.5.122","1.5.123","1.5.124","1.5.125","1.5.126","1.5.127","1.5.128","1.5.129","1.5.13","1.5.130","1.5.131","1.5.132","1.5.133","1.5.134","1.5.135","1.5.136","1.5.137","1.5.138","1.5.139","1.5.14","1.5.140","1.5.141","1.5.142","1.5.143","1.5.144","1.5.145","1.5.146","1.5.147","1.5.148","1.5.149","1.5.15","1.5.16","1.5.17","1.5.18","1.5.19","1.5.2","1.5.20","1.5.21","1.5.22","1.5.23","1.5.24","1.5.25","1.5.26","1.5.27","1.5.28","1.5.29","1.5.3","1.5.30","1.5.31","1.5.32","1.5.33","1.5.34","1.5.35","1.5.36","1.5.37","1.5.38","1.5.39","1.5.40","1.5.41","1.5.42","1.5.43","1.5.44","1.5.45","1.5.46","1.5.47","1.5.48","1.5.49","1.5.5","1.5.50","1.5.51","1.5.52","1.5.53","1.5.54","1.5.55","1.5.56","1.5.57","1.5.58","1.5.59","1.5.6","1.5.60","1.5.61","1.5.62","1.5.63","1.5.64","1.5.65","1.5.66","1.5.67","1.5.68","1.5.69","1.5.7","1.5.70","1.5.71","1.5.72","1.5.73","1.5.74","1.5.75","1.5.76","1.5.77","1.5.78","1.5.79","1.5.8","1.5.80","1.5.81","1.5.82","1.5.83","1.5.84","1.5.85","1.5.86","1.5.87","1.5.88","1.5.89","1.5.9","1.5.90","1.5.91","1.5.92","1.5.93","1.5.94","1.5.95","1.5.96","1.5.97","1.5.98","1.5.99","1.6.1","1.6.10","1.6.11","1.6.12","1.6.13","1.6.14","1.6.15","1.6.16","1.6.17","1.6.18","1.6.19","1.6.2","1.6.20","1.6.21","1.6.22","1.6.23","1.6.24","1.6.25","1.6.26","1.6.27","1.6.28","1.6.29","1.6.3","1.6.30","1.6.31","1.6.32","1.6.33","1.6.34","1.6.35","1.6.36","1.6.37","1.6.38","1.6.39","1.6.4","1.6.40","1.6.41","1.6.42","1.6.43","1.6.44","1.6.45","1.6.46","1.6.47","1.6.48","1.6.5","1.6.50","1.6.51","1.6.52","1.6.53","1.6.54","1.6.55","1.6.56","1.6.57","1.6.58","1.6.6","1.6.7","1.6.8","1.6.9"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/praisonaiagents/PYSEC-2026-3533.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"}]}