GHSA-9mqq-jqxf-grvw PraisonAI MCP `tools/call` path-traversal => RCE via Python `.pth` injection
SummaryPraisonAI'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="../../<some-path>"` 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.Details1. The dispatcher accepts unvalidated kwargs`src/praisonai/praisonai/mcp_server/server.py:281-298`:`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`:`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`):`src/praisonai/praisonai/mcp_server/adapters/__init__.py:33-39`: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`: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**Containment in every cli_tools handler.** Replace bare `os.path.join` / f-string interpolation with explicit prefix validation: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 `..`.**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`.**Reduce the default tool surface.** Move `rules.*` and `workflow.show` behind an explicit `--enable-fs-tools` opt-in. The `register_all` helper should only register read-only safe tools by default.**Require auth on non-loopback HTTP-stream binds.** `praisonai mcp serve --transport http-stream` should refuse to start with `host != 127.0.0.1` if `--api-key` is unset (mirror the gateway's `assert_external_bind_safe` from `src/praisonai/praisonai/gateway/auth.py:23-54`).PoCTested against the PraisonAI repository at HEAD as of 2026-05-02. Verified on Python 3.14 / Windows 11 with both packages installed in editable mode. Each invocation of the RCE chain produced a fresh PID for the spawned Python process — confirmed across four successive runs (PIDs 8172, 23412, 10016, 17912) — proving the payload genuinely runs in a new interpreter, not residual state.Reproduction prerequisitesPython ≥ 3.10 (3.14 used during verification).A clean clone of the PraisonAI repository:Install both packages in editable mode:For PoC #3 (HTTP-stream variant): `pip install uvicorn starlette` (already pulled in by `praisonai[api]`).All other PoCs run against the package source alone — no network server required.PoC 1 — In-process file primitives via MCP `tools/call`Confirms arbitrary file READ, path-traversal WRITE, and path-traversal READ-BACK without spinning up a network server. Equivalent to electerm's parser dry-run; runs against the package source alone.**Expected output (verbatim from this run):**PoC 2 — RCE escalation via Python `.pth`Drops a Python `.pth` payload into the user site-packages directory using the path-traversal write from PoC #1, then spawns an unrelated `python -c "pass"` to demonstrate that the payload runs in a fresh interpreter.**Expected output (verbatim from this run):**The PID in the marker (17912) is the spawned `python -c "pass"` subprocess — not the writing process. Each successive run produces a different PID, proving fresh-interpreter semantics.PoC 3 — End-to-end HTTP-stream variant (default no-auth)Confirms a remote/local attacker who can dial loopback (DNS-rebound browser, container neighbour, malicious local app) reaches the unauth dispatcher and lands the same RCE. The server is started by directly invoking `HTTPStreamTransport` — the same code path that `praisonai mcp serve --transport http-stream` ultimately calls — to keep the PoC stable across CLI-routing changes.**Expected output (verbatim from this run):**The marker contains the PID of a freshly-spawned `python -c "pass"` subprocess (5680 in this run; new PID per run) — RCE in a brand-new interpreter via the unauthenticated HTTP-stream wire.PoC 4 — Indirect prompt-injection variant (the realistic delivery vector)When the praisonai MCP server is wired to Claude Desktop / Cursor / Continue.dev / Claude Code etc., an LLM under indirect prompt injection (web page content, document, email, HTML in scraped data) emits the malicious `tools/call`. **No operator click required beyond ordinary "ask my LLM about this page" usage.**The user cannot tell that the page is malicious — the injection is in an HTML comment. Claude Desktop's standard "approve tool" prompt is the only friction; many MCP client configurations auto-approve `praisonai.rules.create` since it sounds benign.Impact**Arbitrary code execution** on the user's machine, with the user's privileges, on any subsequent Python process they start. The `.pth` payload mechanism makes execution reliable and decoupled in time from the write — the user is not necessarily running `praisonai` when the payload fires; the next `python` invocation suffices.**Arbitrary file read** of any file the user can read — including `~/.ssh/`, `~/.aws/credentials`, `~/.config/praisonai/*.yaml`, environment files, credential stores, source code, browser profiles, IDE workspace state.**Arbitrary file write** anywhere the user can write — plant persistence (`~/.bashrc`, `~/.profile`, Windows Startup folder, `~/Library/LaunchAgents/`, cron, systemd user units, `.ssh/authorized_keys`).**Arbitrary file delete** — destructive / ransomware-style chains.**MCP credential exfiltration**: read the user's MCP client config (`~/Library/Application Support/Claude/claude_desktop_config.json`, Cursor's MCP config, Continue.dev's `.continue/`) which lists every other MCP server the user has wired up — with their API keys / OAuth tokens / credentials. Pivot to those servers.**LLM provider credential exfiltration**: read `~/.config/claude-code/`, OpenAI/Anthropic/Google API keys from environment files and shell rc files.**Default `praisonai mcp serve` configuration** registers the four vulnerable tools unconditionally; no operator misconfiguration is required.The HTTP-stream transport binds to `127.0.0.1` by default but uses the same dispatcher — same-host attackers (other local processes, DNS-rebinding from a browser tab, container neighbours sharing loopback) reach it without authentication.Indirect prompt-injection delivery via web content / documents / emails turns this into a network-borne RCE for any user with an MCP-connected LLM and the praisonai MCP server installed — no link click, no tool approval prompt (depending on MCP client config), no flag flip required beyond the user's normal "ask my LLM about this page" workflow.
CVSS v4.0 score: 9.4
CVSS v3.1 score: 9.6