{"repro_id":"REPRO-2026-00254","version":6,"title":"Vibe-Trading DNS rebinding authentication bypass leading to RCE","repro_type":"security","status":"published","severity":"high","description":"Vibe-Trading before 0.1.10 contains a DNS rebinding authentication bypass vulnerability. An unauthenticated attacker can use DNS rebinding to bypass authentication and then trigger remote code execution. Reproduce: run Vibe-Trading <0.1.10, configure a DNS rebinding setup that targets the application, send requests that bypass the authentication check, and then invoke the code-execution primitive.","root_cause":"# RCA Report — CVE-2026-58169\n\n## Summary\n\nVibe-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.\n\n## Impact\n\n- **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)\n- **Affected versions:** Vibe-Trading < 0.1.10 (confirmed on v0.1.9, commit `8cebccb9b301f962d11bf0fa8c6be9bf7d7e12f2`)\n- **Fixed version:** v0.1.10 (commit `54f944743efee7b9cbaaeafb5966c583d0d4ac66`), via PR #242 (`6e06017`) — adds `_reject_untrusted_loopback_host` middleware\n- **Risk level:** High (CVSS 7.7 / 8.1)\n- **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`)\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** DNS rebinding authentication bypass leading to remote code execution (code_execution) and credential exfiltration\n- **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)...\"`.\n- **Parity:** `full`\n- **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.\n\n## Root Cause\n\nThe root cause is a **loopback peer IP trust without Host header validation**:\n\n1. `_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`.\n\n2. `_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.\n\n3. `_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.\n\n4. **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.\n\n5. The `BashTool.execute()` (line 38 of `bash_tool.py`) calls `subprocess.run(command, shell=True, cwd=cwd, ...)` — arbitrary command execution.\n\n**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).\n\nRelated fixes in the same v0.1.10 cycle:\n- 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\n- PR #245 (`b608fc5`): require bearer token for settings WRITE endpoints — closes the credential exfiltration path\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh` — self-contained, run with `PRUVA_ROOT=<bundle_dir> bash bundle/repro/reproduction_steps.sh`\n2. **What the script does:**\n   - Installs Python dependencies (fastapi, uvicorn, langchain, dnslib, etc.)\n   - Clones/reuses the Vibe-Trading repo from the project cache\n   - Starts a DNS rebinding server (`dns_rebind_server.py`) that resolves `rebind.attacker.test` to `127.0.0.1`\n   - Proves the DNS rebinding with a real DNS A-record query → `127.0.0.1`\n   - 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/...`\n   - **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:\n     - `POST /sessions` → HTTP 201 (auth bypassed, session created)\n     - `POST /sessions/{id}/messages` → triggers the agent loop → BashTool executes the attacker's command → RCE proof file created\n     - `PUT /settings/llm` → auth-bypassed settings write (credential exfiltration path)\n   - **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).\n   - Writes `validation_verdict.json` and `runtime_manifest.json`\n3. **Expected evidence:**\n   - `logs/dns_rebind_proof.txt`: `DNS_A:rebind.attacker.test:127.0.0.1`\n   - `logs/vuln_create_session.txt`: HTTP 201 with session_id (no auth)\n   - `logs/vuln_rce_proof.txt`: `RCE_CONFIRMED`\n   - `logs/vuln_trace.jsonl`: `tool_call: bash` with `exit_code: 0`, `stdout: \"uid=1000(vscode)...\"`\n   - `logs/fixed_create_session.txt`: HTTP 403 `{\"detail\":\"Untrusted local API host\"}`\n   - `logs/fixed_normal_loopback.txt`: HTTP 201 (no regression)\n\n## Evidence\n\nAll artifacts are under `bundle/logs/` and `bundle/repro/`:\n\n| Artifact | Description |\n|---|---|\n| `logs/reproduction_steps.log` | Full script execution log |\n| `logs/dns_rebind_proof.txt` | DNS A-record query proving `rebind.attacker.test → 127.0.0.1` |\n| `logs/vuln_create_session.txt` | Vulnerable: POST /sessions → HTTP 201 (auth bypass) |\n| `logs/vuln_send_message.txt` | Vulnerable: POST /sessions/{id}/messages → HTTP 200 |\n| `logs/vuln_rce_proof.txt` | RCE proof file: `RCE_CONFIRMED` |\n| `logs/vuln_trace.jsonl` | Agent trace showing `tool_call: bash`, `tool_result: exit_code 0, uid=1000(vscode)` |\n| `logs/vuln_session_messages.json` | Session message history (user + assistant reply) |\n| `logs/vuln_settings_write.txt` | Vulnerable: PUT /settings/llm → HTTP 422 (auth bypassed, validation error) |\n| `logs/fixed_create_session.txt` | Fixed: POST /sessions → HTTP 403 \"Untrusted local API host\" |\n| `logs/fixed_normal_loopback.txt` | Fixed: normal loopback → HTTP 201 (no regression) |\n| `logs/fixed_settings_write.txt` | Fixed: PUT /settings/llm → HTTP 403 |\n| `logs/api_server_vulnerable.log` | Vulnerable API server log |\n| `logs/api_server_fixed.log` | Fixed API server log |\n| `logs/mock_llm_server.log` | Mock LLM server log |\n| `logs/dns_rebind_server.log` | DNS rebinding server log |\n\n**Key trace excerpt (vuln_trace.jsonl):**\n```json\n{\"type\": \"tool_call\", \"iter\": 1, \"tool\": \"bash\", \"args\": {\"command\": \"id; whoami; echo RCE_CONFIRMED > /tmp/vibe_trading_rce_proof_49561.txt\", \"run_dir\": \"...\"}}\n{\"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\\\"}\"}\n```\n\n**Vulnerable vs fixed comparison:**\n| Request | v0.1.9 (vulnerable) | v0.1.10 (fixed) |\n|---|---|---|\n| `POST /sessions` (DNS-rebound, no auth) | **201** (auth bypassed) | **403** (\"Untrusted local API host\") |\n| `POST /sessions/{id}/messages` (DNS-rebound) | **200** → BashTool RCE | **403** |\n| `PUT /settings/llm` (DNS-rebound) | **422** (auth bypassed) | **403** |\n| `POST /sessions` (normal loopback `Host: 127.0.0.1`) | 201 | **201** (no regression) |\n\n**Environment:**\n- Python 3.14.4, FastAPI + uvicorn, langchain-openai (ChatOpenAI)\n- Vibe-Trading v0.1.9 (vulnerable) / v0.1.10 (fixed)\n- API server bound to `0.0.0.0:8899` (default), dev mode (no `API_AUTH_KEY`)\n- DNS rebinding server: `dnslib` UDP server on `127.0.0.1:5353`\n- Mock LLM: `http.server` on `127.0.0.1:9099` returning OpenAI-compatible tool-call responses\n\n## Recommendations / Next Steps\n\n1. **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.\n2. **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.\n3. **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.\n4. **Bind to `127.0.0.1` instead of `0.0.0.0`** when the API is not meant to be network-accessible.\n5. **Configure `API_ALLOWED_HOSTS`** if fronting the local API with a specific loopback hostname.\n6. **Testing:** run the security regression tests: `PYTHONPATH=agent python -m pytest agent/tests/test_security_auth_api.py -q`\n\n## Additional Notes\n\n- **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.\n- **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.\n- **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.\n- **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.\n","cve_id":"CVE-2026-58169","cwe_id":"CWE-346 (Origin Validation Error)","source_url":"HKUDS/Vibe-Trading","reproduced_at":"2026-07-06T09:04:26.589776+00:00","duration_secs":1088.0,"tool_calls":239,"handoffs":2,"total_cost_usd":3.8771026699999993,"agent_costs":{"judge":0.0191684,"repro":2.339816789999999,"support":0.1242348,"vuln_variant":1.3938826800000006},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.0191684},"repro":{"accounts/fireworks/routers/glm-5p2-fast":2.339816789999999},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.1242348},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.3938826800000006}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T09:05:06.122877+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":16048,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":12258,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":14744,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":14671,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":18334,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1581,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":3045,"category":"other"},{"path":"bundle/logs/vuln_rce_proof.txt","filename":"vuln_rce_proof.txt","size":14,"category":"other"},{"path":"bundle/logs/vuln_trace.jsonl","filename":"vuln_trace.jsonl","size":1015,"category":"other"},{"path":"bundle/logs/dns_rebind_proof.txt","filename":"dns_rebind_proof.txt","size":37,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1022,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1229,"category":"other"},{"path":"bundle/logs/fixed_create_session.txt","filename":"fixed_create_session.txt","size":47,"category":"other"},{"path":"bundle/logs/vuln_create_session.txt","filename":"vuln_create_session.txt","size":175,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":3158,"category":"log"},{"path":"bundle/logs/vuln_send_message.txt","filename":"vuln_send_message.txt","size":67,"category":"other"},{"path":"bundle/logs/vuln_session_messages.json","filename":"vuln_session_messages.json","size":500,"category":"other"},{"path":"bundle/logs/vuln_settings_write.txt","filename":"vuln_settings_write.txt","size":159,"category":"other"},{"path":"bundle/logs/fixed_normal_loopback.txt","filename":"fixed_normal_loopback.txt","size":175,"category":"other"},{"path":"bundle/logs/fixed_settings_write.txt","filename":"fixed_settings_write.txt","size":47,"category":"other"},{"path":"bundle/logs/api_server_vulnerable.log","filename":"api_server_vulnerable.log","size":1121,"category":"log"},{"path":"bundle/logs/api_server_fixed.log","filename":"api_server_fixed.log","size":803,"category":"log"},{"path":"bundle/logs/mock_llm_server.log","filename":"mock_llm_server.log","size":119,"category":"log"},{"path":"bundle/logs/dns_rebind_server.log","filename":"dns_rebind_server.log","size":83,"category":"log"},{"path":"bundle/logs/vuln_variant/fixed_optedin_ts_rce_proof.txt","filename":"fixed_optedin_ts_rce_proof.txt","size":14,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_default_host_testserver.txt","filename":"fixed_default_host_testserver.txt","size":191,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_default_host_rebind.attacker.test.txt","filename":"fixed_default_host_rebind.attacker.test.txt","size":73,"category":"other"},{"path":"bundle/logs/vuln_variant/variant_steps.log","filename":"variant_steps.log","size":2962,"category":"log"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4548,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3676,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":11475,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":3724,"category":"other"},{"path":"bundle/logs/vuln_variant/vuln_host_rebind.attacker.test.txt","filename":"vuln_host_rebind.attacker.test.txt","size":201,"category":"other"},{"path":"bundle/logs/vuln_variant/vuln_host_testserver.txt","filename":"vuln_host_testserver.txt","size":191,"category":"other"},{"path":"bundle/logs/vuln_variant/vuln_host_localhost.txt","filename":"vuln_host_localhost.txt","size":190,"category":"other"},{"path":"bundle/logs/vuln_variant/vuln_rce_proof.txt","filename":"vuln_rce_proof.txt","size":14,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_default_host_localhost.txt","filename":"fixed_default_host_localhost.txt","size":190,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_default_ts_msg.txt","filename":"fixed_default_ts_msg.txt","size":74,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_optedin_host_rebind.attacker.test.txt","filename":"fixed_optedin_host_rebind.attacker.test.txt","size":73,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_optedin_host_testserver.txt","filename":"fixed_optedin_host_testserver.txt","size":191,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_optedin_ts_msg.txt","filename":"fixed_optedin_ts_msg.txt","size":74,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_optedin_ts_state.json","filename":"fixed_optedin_ts_state.json","size":25,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_optedin_ts_req.json","filename":"fixed_optedin_ts_req.json","size":88,"category":"other"},{"path":"bundle/logs/vuln_variant/api_server_vuln.log","filename":"api_server_vuln.log","size":1121,"category":"log"},{"path":"bundle/logs/vuln_variant/api_server_fixed-default.log","filename":"api_server_fixed-default.log","size":2532,"category":"log"},{"path":"bundle/logs/vuln_variant/api_server_fixed-optedin.log","filename":"api_server_fixed-optedin.log","size":2532,"category":"log"},{"path":"bundle/logs/vuln_variant/mock_llm_server.log","filename":"mock_llm_server.log","size":0,"category":"log"}]}