# RCA Report — CVE-2026-58169

## Summary

Vibe-Trading before v0.1.10 contains a DNS rebinding authentication bypass vulnerability (CWE-346 Origin Validation Error). The FastAPI API server (`agent/api_server.py`) binds to `0.0.0.0` by default and treats any TCP connection from a loopback peer address (`127.0.0.1`, `::1`, `localhost`) as a trusted local/dev-mode caller, bypassing `API_AUTH_KEY` bearer-token authentication entirely. Because the server performs **no Host header validation** on loopback-trusted requests, an attacker can use DNS rebinding to make a victim's browser issue same-origin JSON requests to the local API with an attacker-controlled `Host` header and no bearer token. The loopback peer IP causes `_is_local_client()` to return `True`, so `_validate_api_auth()` skips authentication. Furthermore, `_shell_tools_enabled_for_request()` returns `True` for loopback clients, enabling the `bash` tool in the agent's tool registry. The attacker can then send a message to a session that triggers the `BashTool`, which executes `subprocess.run(command, shell=True)` — achieving remote code execution as the API process user. The attacker can also overwrite LLM and data-source settings to exfiltrate credentials.

## Impact

- **Package/component affected:** `HKUDS/Vibe-Trading` — `agent/api_server.py` (loopback trust/auth boundary, Host-header validation), `agent/src/tools/bash_tool.py` (BashTool RCE primitive), `agent/src/tools/__init__.py` (shell tool gating)
- **Affected versions:** Vibe-Trading < 0.1.10 (confirmed on v0.1.9, commit `8cebccb9b301f962d11bf0fa8c6be9bf7d7e12f2`)
- **Fixed version:** v0.1.10 (commit `54f944743efee7b9cbaaeafb5966c583d0d4ac66`), via PR #242 (`6e06017`) — adds `_reject_untrusted_loopback_host` middleware
- **Risk level:** High (CVSS 7.7 / 8.1)
- **Consequences:** Unauthenticated remote code execution as the API process user; unauthorized access to privileged control-plane endpoints (`/live/runner/start`, `/swarm/runs`, `/sessions/{id}/messages`); credential exfiltration via settings overwrite (`/settings/llm`, `/settings/data-sources`)

## Impact Parity

- **Disclosed/claimed maximum impact:** DNS rebinding authentication bypass leading to remote code execution (code_execution) and credential exfiltration
- **Reproduced impact from this run:** Full chain demonstrated — DNS rebinding → auth bypass → shell tools enabled → BashTool executes arbitrary shell command as API process user (`uid=1000(vscode)`). The proof file `/tmp/vibe_trading_rce_proof_*.txt` was created with `RCE_CONFIRMED`. The agent trace shows `tool_call: bash` with `exit_code: 0` and `stdout: "uid=1000(vscode)..."`.
- **Parity:** `full`
- **Not demonstrated:** The reproduction used a mock LLM server (simulating an attacker-controlled LLM endpoint) to instruct the agent to invoke the bash tool. In a real attack, the attacker would either poison the LLM settings (via the auth-bypassed `/settings/llm` PUT) to point the agent at an attacker-controlled LLM, or craft a user message that persuades the agent to run a shell command. The code-execution primitive (BashTool with `subprocess.run(shell=True)`) is the same either way.

## Root Cause

The root cause is a **loopback peer IP trust without Host header validation**:

1. `_is_local_client(request)` (line 668) checks only `request.client.host` (the TCP peer address). If it is `127.0.0.1`, `::1`, `localhost`, or a trusted Docker gateway, it returns `True`.

2. `_validate_api_auth()` (line 645): when `API_AUTH_KEY` is not configured (dev mode), a loopback client is allowed without any token. When `API_AUTH_KEY` IS configured, the loopback shortcut is not used — but in dev mode (the default), the bypass is complete.

3. `_shell_tools_enabled_for_request(request)` (line 727): returns `_is_local_client(request) or _env_shell_tools_enabled()`. For loopback clients, this is `True`, enabling the `bash` and `background_run` tools in the agent registry.

4. **No Host header validation existed** — a loopback request carrying `Host: attacker.example:8899` was trusted as local. DNS rebinding makes a browser send exactly such a request: the attacker's domain resolves to `127.0.0.1`, the browser connects from loopback, and sends `Host: attacker.example:8899` (the original hostname) with no bearer token.

5. The `BashTool.execute()` (line 38 of `bash_tool.py`) calls `subprocess.run(command, shell=True, cwd=cwd, ...)` — arbitrary command execution.

**Fix (PR #242, commit `6e06017`):** Added `_reject_untrusted_loopback_host` middleware that rejects loopback requests carrying an untrusted `Host` header with HTTP 403 ("Untrusted local API host") before route handlers are reached. Accepted loopback Host values: `localhost`, `127.0.0.1`, `::1`, `[::1]`, `testserver`, plus any configured via `API_ALLOWED_HOSTS`. Remote clients remain governed by bearer-token auth. Normal loopback dev-mode requests with `Host: 127.0.0.1:port` continue to work (no regression).

Related fixes in the same v0.1.10 cycle:
- PR #243 (`7eb8dea`): require explicit opt-in (`VIBE_TRADING_ENABLE_SHELL_TOOLS`) for agent shell tools — closes the BashTool exposure even if the Host gate is bypassed
- PR #245 (`b608fc5`): require bearer token for settings WRITE endpoints — closes the credential exfiltration path

## Reproduction Steps

1. **Script:** `bundle/repro/reproduction_steps.sh` — self-contained, run with `PRUVA_ROOT=<bundle_dir> bash bundle/repro/reproduction_steps.sh`
2. **What the script does:**
   - Installs Python dependencies (fastapi, uvicorn, langchain, dnslib, etc.)
   - Clones/reuses the Vibe-Trading repo from the project cache
   - Starts a DNS rebinding server (`dns_rebind_server.py`) that resolves `rebind.attacker.test` to `127.0.0.1`
   - Proves the DNS rebinding with a real DNS A-record query → `127.0.0.1`
   - Starts a mock OpenAI-compatible LLM server (`mock_llm_server.py`) that returns a `bash` tool-call instructing the agent to execute `id; whoami; echo RCE_CONFIRMED > /tmp/...`
   - **Phase 1 (vulnerable v0.1.9):** Starts the real Vibe-Trading API server (bound to `0.0.0.0:8899`, dev mode, mock LLM). Uses `curl --resolve rebind.attacker.test:8899:127.0.0.1` to send DNS-rebound HTTP requests with no bearer token:
     - `POST /sessions` → HTTP 201 (auth bypassed, session created)
     - `POST /sessions/{id}/messages` → triggers the agent loop → BashTool executes the attacker's command → RCE proof file created
     - `PUT /settings/llm` → auth-bypassed settings write (credential exfiltration path)
   - **Phase 2 (fixed v0.1.10):** Same DNS-rebound requests → HTTP 403 ("Untrusted local API host"). Normal loopback with `Host: 127.0.0.1:8899` → HTTP 201 (no regression).
   - Writes `validation_verdict.json` and `runtime_manifest.json`
3. **Expected evidence:**
   - `logs/dns_rebind_proof.txt`: `DNS_A:rebind.attacker.test:127.0.0.1`
   - `logs/vuln_create_session.txt`: HTTP 201 with session_id (no auth)
   - `logs/vuln_rce_proof.txt`: `RCE_CONFIRMED`
   - `logs/vuln_trace.jsonl`: `tool_call: bash` with `exit_code: 0`, `stdout: "uid=1000(vscode)..."`
   - `logs/fixed_create_session.txt`: HTTP 403 `{"detail":"Untrusted local API host"}`
   - `logs/fixed_normal_loopback.txt`: HTTP 201 (no regression)

## Evidence

All artifacts are under `bundle/logs/` and `bundle/repro/`:

| Artifact | Description |
|---|---|
| `logs/reproduction_steps.log` | Full script execution log |
| `logs/dns_rebind_proof.txt` | DNS A-record query proving `rebind.attacker.test → 127.0.0.1` |
| `logs/vuln_create_session.txt` | Vulnerable: POST /sessions → HTTP 201 (auth bypass) |
| `logs/vuln_send_message.txt` | Vulnerable: POST /sessions/{id}/messages → HTTP 200 |
| `logs/vuln_rce_proof.txt` | RCE proof file: `RCE_CONFIRMED` |
| `logs/vuln_trace.jsonl` | Agent trace showing `tool_call: bash`, `tool_result: exit_code 0, uid=1000(vscode)` |
| `logs/vuln_session_messages.json` | Session message history (user + assistant reply) |
| `logs/vuln_settings_write.txt` | Vulnerable: PUT /settings/llm → HTTP 422 (auth bypassed, validation error) |
| `logs/fixed_create_session.txt` | Fixed: POST /sessions → HTTP 403 "Untrusted local API host" |
| `logs/fixed_normal_loopback.txt` | Fixed: normal loopback → HTTP 201 (no regression) |
| `logs/fixed_settings_write.txt` | Fixed: PUT /settings/llm → HTTP 403 |
| `logs/api_server_vulnerable.log` | Vulnerable API server log |
| `logs/api_server_fixed.log` | Fixed API server log |
| `logs/mock_llm_server.log` | Mock LLM server log |
| `logs/dns_rebind_server.log` | DNS rebinding server log |

**Key trace excerpt (vuln_trace.jsonl):**
```json
{"type": "tool_call", "iter": 1, "tool": "bash", "args": {"command": "id; whoami; echo RCE_CONFIRMED > /tmp/vibe_trading_rce_proof_49561.txt", "run_dir": "..."}}
{"type": "tool_result", "iter": 1, "tool": "bash", "status": "ok", "elapsed_ms": 4, "preview": "{\"status\": \"ok\", \"exit_code\": 0, \"stdout\": \"uid=1000(vscode) gid=1000(vscode) groups=1000(vscode),969(969)\\nvscode\\n\", \"stderr\": \"id: cannot find name for group ID 969\\n\"}"}
```

**Vulnerable vs fixed comparison:**
| Request | v0.1.9 (vulnerable) | v0.1.10 (fixed) |
|---|---|---|
| `POST /sessions` (DNS-rebound, no auth) | **201** (auth bypassed) | **403** ("Untrusted local API host") |
| `POST /sessions/{id}/messages` (DNS-rebound) | **200** → BashTool RCE | **403** |
| `PUT /settings/llm` (DNS-rebound) | **422** (auth bypassed) | **403** |
| `POST /sessions` (normal loopback `Host: 127.0.0.1`) | 201 | **201** (no regression) |

**Environment:**
- Python 3.14.4, FastAPI + uvicorn, langchain-openai (ChatOpenAI)
- Vibe-Trading v0.1.9 (vulnerable) / v0.1.10 (fixed)
- API server bound to `0.0.0.0:8899` (default), dev mode (no `API_AUTH_KEY`)
- DNS rebinding server: `dnslib` UDP server on `127.0.0.1:5353`
- Mock LLM: `http.server` on `127.0.0.1:9099` returning OpenAI-compatible tool-call responses

## Recommendations / Next Steps

1. **Upgrade to v0.1.10 or later** — the `_reject_untrusted_loopback_host` middleware (PR #242) blocks DNS-rebinding Host headers on loopback peers with HTTP 403.
2. **Set `API_AUTH_KEY`** in any deployment exposed beyond a single-user localhost — this forces bearer-token auth even for loopback clients when configured, removing the dev-mode bypass.
3. **Set `VIBE_TRADING_ENABLE_SHELL_TOOLS=1` only when explicitly needed** — PR #243 requires explicit opt-in for the BashTool; without it, the RCE primitive is unavailable even if auth is bypassed.
4. **Bind to `127.0.0.1` instead of `0.0.0.0`** when the API is not meant to be network-accessible.
5. **Configure `API_ALLOWED_HOSTS`** if fronting the local API with a specific loopback hostname.
6. **Testing:** run the security regression tests: `PYTHONPATH=agent python -m pytest agent/tests/test_security_auth_api.py -q`

## Additional Notes

- **Idempotency:** The script was run twice consecutively; both runs produced identical results (HTTP 201 + RCE on vulnerable, HTTP 403 on fixed). The script cleans up all background processes on exit and uses a PID-suffixed proof file to avoid collisions.
- **Mock LLM justification:** The reproduction uses a mock OpenAI-compatible LLM server that returns a `bash` tool-call. This simulates the attack scenario where the attacker either (a) poisons the LLM settings via the auth-bypassed `/settings/llm` PUT to point at an attacker-controlled LLM, or (b) sends a user message that the agent's LLM responds to with a bash tool-call. The BashTool's `subprocess.run(shell=True)` is the actual RCE primitive in both cases — the mock LLM merely drives the agent loop to reach it.
- **The `PUT /settings/llm` returned 422** (not 200) on the vulnerable version because the request body did not match the full Pydantic model schema, but the important point is that the request **passed authentication** (no 401/403) — the auth bypass is confirmed. On the fixed version, the same request returns 403 at the Host-gate middleware before reaching the route handler.
- **CORS does not defend against DNS rebinding** — after rebinding, the attacker's hostname becomes the browser's same origin, so credentialed CORS requests are sent without cross-origin restrictions.
