GitHub Advisoryadvisory · PyPI
GHSA-9mqq-jqxf-grvw PraisonAI MCP `tools/call` path-traversal => RCE via Python `.pth` injection
CRITICAL
Official page ↗
- Aliases
- CVE-2026-44336
- Ecosystem
- PyPI
- Packages
- PraisonAI
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-20, CWE-22, CWE-829, CWE-913, CWE-94
- Reviewed
- 2026-05-11
Affected packages
• PyPI PraisonAI: < 4.6.34 → fixed in 4.6.34
Description
Summary
PraisonAI's MCP (Model Context Protocol) server (`praisonai mcp serve`) registers four file-handling tools by default — `praisonai.rules.create`, `praisonai.rules.show`, `praisonai.rules.delete`, and `praisonai.workflow.show`. Each accepts a path or filename string from MCP `tools/call` arguments and joins it onto `~/.praison/rules/` (or, for `workflow.show`, accepts an absolute path) with no containment check. The JSON-RPC dispatcher passes `params["arguments"]` blind to each handler via `kwargs` without validating against the advertised input schema.
By setting `rule_name="../../"` an attacker walks out of the rules directory and writes any file the running user can write. Dropping a Python `.pth` file into the user site-packages directory escalates this primitive to arbitrary code execution in any subsequent Python process the user spawns — the next `praisonai` CLI invocation, an IDE script run, the user's `python` REPL, or any background Python service. The same primitive is reachable from:
• An MCP-connected LLM (Claude Desktop, Cursor, Continue.dev, Claude Code) whose context is poisoned by attacker-controlled web content / documents / emails — no operator click required beyond ordinary "ask the LLM to summarise this page" usage.
• `praisonai mcp serve --transport http-stream` with no `--api-key` (default), reachable from any local process / DNS-rebound browser tab / container neighbour sharing loopback.
• Stdio MCP from any prompt-injection vector that reaches the connected LLM.
No operator misconfiguration is required. No env var, flag, or config switch disables the vulnerable handlers.
---
Details
1. The dispatcher accepts unvalidated kwargs
`src/praisonai/praisonai/mcp_server/server.py:281-298`:
```python
async def _handle_tools_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle tools/call request."""
tool_name = params.get("name")
arguments = params.get("arguments", {})
if not tool_name:
raise ValueError("Tool name required")
tool = self._tool_registry.get(tool_name)
if tool is None:
raise ValueError(f"Tool not found: {tool_name}")
# Execute tool
try:
if asyncio.iscoroutinefunction(tool.handler):
result = await tool.handler(arguments) # ← no schema enforcement
else:
result = tool.handler(arguments)
```
`tool.input_schema` is built reflectively from the handler signature in `registry.py:320-376` and surfaced in `tools/list` responses — but it is never enforced before dispatch. Whatever JSON shape the MCP client (or an LLM under prompt injection) sends becomes a `kwargs` call.
2. The four registered handlers have no containment
`src/praisonai/praisonai/mcp_server/adapters/cli_tools.py`:
```python
line 116-128 — rules.create — primary write primitive
@register_tool("praisonai.rules.create")
def rules_create(rule_name: str, content: str) -> str:
"""Create a new rule."""
try:
import os
rules_dir = os.path.expanduser("~/.praison/rules")
os.makedirs(rules_dir, exist_ok=True)
rule_path = os.path.join(rules_dir, rule_name) # ← no realpath/containment
with open(rule_path, 'w') as f:
f.write(content)
return f"Rule created: {rule_name}"
except Exception as e:
return f"Error: {e}"
line 102-114 — rules.show — read primitive (f-string interpolation, same vuln class)
@register_tool("praisonai.rules.show")
def rules_show(rule_name: str) -> str:
"""Show a specific rule."""
try:
import os
rule_path = os.path.expanduser(f"~/.praison/rules/{rule_name}") # ← `..` works
if not os.path.exists(rule_path):
return f"Rule not found: {rule_name}"
with open(rule_path, 'r') as f:
content = f.read()
return content
except Exception as e:
return f"Error: {e}"
line 130-141 — rules.delete — delete primitive
@register_tool("praisonai.rules.delete")
def rules_delete(rule_name: str) -> str:
"""Delete a rule."""
try:
import os
rule_path = os.path.expanduser(f"~/.praison/rules/{rule_name}") # ← same pattern
if not os.path.exists(rule_path):
return f"Rule not found: {rule_name}"
os.remove(rule_path)
return f"Rule deleted: {rule_name}"
except Exception as e:
return f"Error: {e}"
line 63-73 — workflow.show — absolute-path read primitive (no traversal needed)
@register_tool("praisonai.workflow.show")
def workflow_show(file_path: str) -> str:
"""Show workflow configuration."""
try:
with open(file_path, 'r') as f: # ← absolute path, no validation
content = f.read()
return content
except FileNotFoundError:
return f"File not found: {file_path}"
except Exception as e:
return f"Error: {e}"
```
`os.path.join(rules_dir, "../../somewhere")` and `os.path.expanduser(f"~/.praison/rules/../../somewhere")` both resolve `..` segments at `open()` time, so the on-disk effect escapes the rules directory. `workflow.show` does not need traversal at all — it `open()`s an absolute path the LLM supplied.
3. Default registration ships these unconditionally
`src/praisonai/praisonai/mcp_server/cli.py:216-219` (`cmd_serve`):
```python
from .adapters import register_all
register_all()
```
`src/praisonai/praisonai/mcp_server/adapters/__init__.py:33-39`:
```python
def _register_all():
register_all_tools()
register_extended_capability_tools()
register_cli_tools() # ← rules.create / rules.show / rules.delete / workflow.show
register_mcp_resources()
register_mcp_prompts()
```
There is no flag, env var, or config switch that disables the file primitives. `praisonai mcp serve` registers them on every startup.
4. HTTP-stream transport defaults to no authentication
`src/praisonai/praisonai/mcp_server/cli.py:184`:
```python
parser.add_argument("--api-key", default=None)
```
The auth check at `mcp_server/transports/http_stream.py:191-198` is wrapped in `if self.api_key:` — `None` skips the entire block. Default config: `praisonai mcp serve --transport http-stream` binds `127.0.0.1:8080/mcp` unauthenticated.
5. Code-execution escalation via Python `.pth`
CPython's `Lib/site.py` (`addsitedir` / `addpackage`) imports lines starting with `import` from every `.pth` file present in `site.getsitepackages()` and `site.getusersitepackages()` at every interpreter startup. The user site-packages directory is always writable without elevation. A single `.pth` file containing `import os; os.system("...")` turns the path-traversal write primitive into RCE on the next Python interpreter the user starts — including the user's own `python` REPL, the next `praisonai` CLI command, IDE script launchers, and any background Python service.
---
Suggested fix
1. Containment in every cli_tools handler. Replace bare `os.path.join` / f-string interpolation with explicit prefix validation:
```python
import re
from pathlib import Path
if not re.fullmatch(r"[A-Za-z0-9._-]+", rule_name):
return "Error: invalid rule name"
rules_dir = Path(os.path.expanduser("~/.praison/rules")).resolve()
rule_path = (rules_dir / rule_name).resolve()
if not str(rule_path).startswith(str(rules_dir) + os.sep):
return "Error: rule_name escapes rules directory"
```
Apply identically to `praisonai.rules.create`, `rules.show`, `rules.delete`, `workflow.validate`. For `workflow.show`, restrict `file_path` to a designated workflow directory and reject absolute paths or any value containing `..`.
2. Schema enforcement in the dispatcher. Validate `params["arguments"]` against `tool.input_schema` (a JSON-Schema validator such as `jsonschema`) before `tool.handler(arguments)`. Reject unknown properties, type mismatches, missing required fields. Return JSON-RPC `-32602 Invalid params`.
3. …
Related
Alias · CVE (1)
- CVE-2026-44336 PraisonAI is a multi-agent teams system. vulnerability
Weakness · CWE (5)
- CWE-20 Improper Input Validation weakness
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness
- CWE-829 Inclusion of Functionality from Untrusted Control Sphere weakness
- CWE-913 Improper Control of Dynamically-Managed Code Resources weakness
- CWE-94 Improper Control of Generation of Code weakness