PYSEC-2026-3517 
# Unauthenticated PraisonAI UI MCP connect endpoint executes attacker-chosen local commands

## Summary

PraisonAI v4.6.48 exposes the PraisonAIUI MCP client management API through the default UI host apps without authentication. A remote unauthenticated client can send `POST /api/mcp/connect` with a `command` and `args` field. The endpoint passes those values into the MCP stdio client, which starts the attacker-selected local process as the PraisonAI UI service user.

The issue is reachable through PraisonAI's hosted UI integration (`praisonai ui`, `praisonai ui agents`, `praisonai claw`, and any app using `praisonai.integration.host_app.create_host_app()` / `build_host_app()`). `praisonai ui` and related Typer UI commands bind to `0.0.0.0` by default.

## Affected Versions

Confirmed affected:

- `praisonai` v4.6.48
- Commit tested: `d5f1114aaf1a2e9f121a6e66b929149ca2201f1d`
- Tag tested: `v4.6.48`
- Pinned UI dependency: `aiui==0.3.121` from `src/praisonai/uv.lock`

Likely affected:

- Any PraisonAI release that exposes `aiui` / `praisonaiui` `create_app()` through the PraisonAI UI host apps without authentication and includes the `mcp` dependency. I only confirmed the latest release during this audit.

## Severity

Reasoning:

- `AV`: the vulnerable endpoint is an HTTP API route.
- `AC`: a single POST request is sufficient.
- `PR`: default UI host apps do not require credentials unless opt-in auth is configured.
- `UI`: no victim interaction is needed after the server is running.
- `S`: code executes in the PraisonAI UI server process context.
- `C/I/A`: arbitrary local command execution permits secret exfiltration, file tampering, and service disruption.

## Root Cause

PraisonAI depends on MCP by default and exposes PraisonAIUI via optional UI extras:

- `src/praisonai/pyproject.toml:11` includes base dependencies.
- `src/praisonai/pyproject.toml:19` includes `mcp>=1.20.0`.
- `src/praisonai/pyproject.toml:25` defines the `ui` extra with `aiui>=0.3.121,<0.4`.
- `src/praisonai/pyproject.toml:197` defines the `claw` extra with `aiui[all]>=0.3.121,<0.4`.

PraisonAI's UI commands bind externally by default and launch `aiui run`:

- `src/praisonai/praisonai/cli/commands/ui.py:114` sets `host="0.0.0.0"` for `praisonai ui`.
- `src/praisonai/praisonai/cli/commands/ui.py:163` passes that host to `aiui run`.
- `src/praisonai/praisonai/cli/commands/ui.py:186`, `:204`, and `:222` also default subcommands to `0.0.0.0`.
- `src/praisonai/praisonai/cli/commands/claw.py:41` defines the full dashboard command.
- `src/praisonai/praisonai/cli/commands/claw.py:93` launches `aiui run` with the selected host.

PraisonAI's default apps create the PraisonAIUI Starlette app without forcing authentication:

- `src/praisonai/praisonai/ui_chat/default_app.py:18` calls `configure_host(...)`.
- `src/praisonai/praisonai/ui_chat/default_app.py:142` exports `app = create_host_app()`.
- `src/praisonai/praisonai/claw/default_app.py:63` calls `configure_host(...)`.
- `src/praisonai/praisonai/claw/default_app.py:128` exports `app = create_host_app()`.
- `src/praisonai/praisonai/integration/host_app.py:174` imports `praisonaiui.server.create_app`.
- `src/praisonai/praisonai/integration/host_app.py:180` returns `create_app()`.

In `aiui==0.3.121`, the exposed server registers the MCP routes and auth is opt-in:

- `praisonaiui/server.py:1483` defines `api_mcp_connect`.
- `praisonaiui/server.py:1488` reads attacker-controlled JSON.
- `praisonaiui/server.py:1491` accepts either `command` or `url`.
- `praisonaiui/server.py:1496` calls `connect_mcp_server(body)`.
- `praisonaiui/server.py:2516` defines `create_app(..., require_auth=False, ...)`.
- `praisonaiui/server.py:2550` adds `AuthEnforcementMiddleware`, but it only enforces auth when `AUTH_ENFORCE=true`.
- `praisonaiui/server.py:2769` registers `/api/mcp/servers`.
- `praisonaiui/server.py:2770` registers `/api/mcp/connect`.

The MCP feature converts the request body into a local process launch:

- `praisonaiui/features/mcp.py:325` defines `connect_server(self, server_config)`.
- `praisonaiui/features/mcp.py:330` chooses stdio transport when `command` is present.
- `praisonaiui/features/mcp.py:332` constructs `StdioMCPClient(command=server_config["command"], args=server_config.get("args", []))`.
- `praisonaiui/features/mcp.py:360` calls `client.connect()`, which invokes the MCP stdio transport and starts the process.

## Minimal PoC

PoC file: `poc/praisonai-aiui-mcp-connect-rce.py`

The PoC runs the PraisonAI host app in-process, sends the unauthenticated HTTP request, and asks the server to execute `/usr/bin/touch /tmp/praisonai_host_app_mcp_touch_marker.txt`. It does not contact an LLM provider and uses no credentials.

Observed output from the tested checkout with `aiui==0.3.121` and `mcp==1.25.0` available:

```text
[19:19:55] server.py:229 WARNING No auth_token provided for Gateway server. Generated temporary token: gw_****650a. For production, set GATEWAY_AUTH_TOKEN.
[19:19:55] mcp.py:135 ERROR Failed to connect to MCP stdio server: 'tuple' object has no attribute 'initialize'
HTTP_STATUS= 200
RESPONSE= {"server":{"name":"poc-stdio-process-0","transport":"stdio","status":"error","tools":[],"last_error":"Connection failed"}}
SUCCESS_AT_ATTEMPT= 0
MARKER_EXISTS= True
MARKER_PATH= /tmp/praisonai_host_app_mcp_touch_marker.txt
```

The MCP handshake fails because `aiui==0.3.121` is not compatible with the locked `mcp==1.25.0` return shape, but the attacker-selected process is already started. Process startup can race the immediate teardown caused by this version mismatch, so the checked-in PoC retries the same unauthenticated request until `/usr/bin/touch` wins scheduling and creates the marker. The marker file proves local command execution despite the reported MCP connection error.

## Exploit Scenario

An operator runs:

```bash
pip install "praisonai[ui]"
praisonai ui
```

Because `praisonai ui` binds to `0.0.0.0` by default and the generated app does not require authentication by default, any host that can reach the UI port can send:

```http
POST /api/mcp/connect
Content-Type: application/json

{
  "name": "evil",
  "command": "/usr/bin/touch",
  "args": ["/tmp/pwned-by-ui-mcp"]
}
```

In a real attack, the command can be replaced with a shell, a credential exfiltration command, a file modification command, or a payload that starts a long-lived process as the PraisonAI UI server user.

## Novelty / Non-Duplicate Analysis

Searched sources:

- OSV query for PyPI `praisonai`: 51 advisories returned.
- OSV query for PyPI `aiui`: 0 advisories returned.
- OSV query for PyPI `praisonaiui`: 0 advisories returned.
- GitHub Advisory Database search for exact `/api/mcp/connect`, `api_mcp_connect`, `StdioMCPClient`, `connect_mcp_server`, and `praisonaiui.features.mcp`.
- NVD API searches for `PraisonAI StdioMCPClient`, `PraisonAI api_mcp_connect`, `PraisonAI /api/mcp/connect`, `PraisonAIUI /api/mcp/connect`, and `aiui StdioMCPClient`: 0 results.
- GitHub issue/PR searches in `MervinPraison/PraisonAI` for exact endpoint/function/class terms. Only one unrelated PR was returned for `/api/mcp/connect`; no issue/PR matched `api_mcp_connect`, `StdioMCPClient`, `connect_mcp_server`, or `praisonaiui.features.mcp`.
- Broad web searches for exact endpoint, file, class, and function terms returned no matching public vulnerability report.

Why this is distinct from known PraisonAI advisories:

- Not the excluded `praisonai serve agents --api-key` `/agents` auth bypass. This report targets `POST /api/mcp/connect` in the PraisonAIUI host app.
- Not GHSA-9gm9-c8mq-vq7m / CVE-2026-34935 or GHSA-9qhq-v63v-fv3j / CVE-2026-41497. Those involve `MCPHandler.parse_mcp_command()` command parsing. This finding uses `praisonaiui.server.api_mcp_connect -> praisonaiui.features.mcp.connect_mcp_server -> StdioMCPClient`.
- Not GHSA-pj2r-f9mw-vrcq / CVE-2026-40159. That advisory concerns sensitive environment variables inherited by untrusted MCP subprocesses. This finding is unauthenticated network-triggered local process execution.
- Not GHSA-6rmh-7xcm-cpxj or GHSA-8444-4fhq-fxpq. Those concern unauthenticated legacy/generated agent servers. This is a distinct UI route and a distinct sink that starts arbitrary local processes.
- Not GHSA-9cr9-25q5-8prj, GHSA-9mqq-jqxf-grvw, or other MCP server file-read/path-traversal advisories. This path is the UI MCP client connector, not PraisonAI's MCP server tool dispatcher.

## Recommended Fix

1. Remove arbitrary `command`/`args` from the remote HTTP API. MCP stdio servers should be configured only from trusted local configuration, not caller-supplied JSON.
2. Require authentication and authorization on `/api/mcp/connect`, `/api/mcp/disconnect/*`, and `/api/mcp/servers` regardless of `AUTH_ENFORCE`.
3. Change UI command defaults from `0.0.0.0` to `127.0.0.1`, or require an explicit `--unsafe-expose` style flag when binding externally without auth.
4. If remote MCP registration is a required feature, allow only URL-based transports with SSRF protections, or maintain an administrator-configured allowlist of commands.
5. Add regression tests that unauthenticated requests to `/api/mcp/connect` cannot start a subprocess, including when `AUTH_ENFORCE` is unset.
CVSS v3.1 score: 9.8