# Variant RCA Report — CVE-2026-58169 (Vibe-Trading DNS Rebinding Auth Bypass → RCE)

## Summary

A **partial bypass** of the v0.1.10 Host-validation fix (PR #242) was confirmed:
the production loopback-Host allowlist `_DEFAULT_LOOPBACK_HOSTS` includes
`"testserver"` — a non-loopback hostname added for Starlette `TestClient`
convenience. A loopback peer request carrying `Host: testserver:<port>` and no
bearer token passes the `_reject_untrusted_loopback_host` middleware and is
auth-bypassed on the **fixed v0.1.10** (`POST /sessions` → HTTP 201), exactly
where an attacker-domain Host is correctly rejected (HTTP 403). In the default
configuration the independent shell-tools opt-in (PR #243) still blocks the
`BashTool` RCE primitive, so the partial bypass does **not** yield RCE by
default. However, when the operator has enabled `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`
(the exact scenario the opt-in exists for), the `testserver` Host bypass
restores the **full RCE chain** on the fixed v0.1.10 — demonstrated end-to-end
(`Host: testserver` → session → agent message → `BashTool`
`subprocess.run(shell=True)` → `RCE_CONFIRMED`). This is a **conditional** bypass:
in the standard DNS-rebinding model the attacker cannot deliver
`Host: testserver` (they control an attacker-owned domain, not the bare label
`testserver`), so the *standard* DNS-rebinding RCE bypass is **not** confirmed.
The finding is a genuine incompleteness in the fix's allowlist that should be
closed.

## Fix Coverage / Assumptions

**Invariant the fix relies on (PR #242):** a loopback peer may carry only a
Host that names a loopback address (or an operator-configured
`API_ALLOWED_HOSTS` name); any other Host on a loopback peer is a DNS-rebinding
artifact and is rejected with HTTP 403 before route handlers run.

**Code paths explicitly covered:** every HTTP request passes through
`_reject_untrusted_loopback_host` (`@app.middleware("http")`,
`agent/api_server.py:562`). Loopback peers with a Host outside
`_DEFAULT_LOOPBACK_HOSTS ∪ _EXTRA_LOOPBACK_HOSTS` are rejected. Non-loopback
peers pass through to per-route `_validate_api_auth`, which requires
`API_AUTH_KEY` for non-local access. Secondary defenses: PR #243 gates
`bash`/`background_run` behind `VIBE_TRADING_ENABLE_SHELL_TOOLS`; PR #245
requires a bearer token for settings writes.

**What the fix does NOT cover:** the allowlist trusts `"testserver"`, a
non-loopback test hostname. This violates the fix's own invariant and lets a
loopback peer with `Host: testserver` be auth-bypassed on the fixed version.

## Variant / Alternate Trigger

**Variant ID:** `VT-CVE-2026-58169-testserver-host-allowlist`

**Entry point:** `POST /sessions/{id}` and `POST /sessions/{id}/messages` over
the Vibe-Trading API (`agent/api_server.py`), reached by a loopback peer
(`127.0.0.1`) sending `Host: testserver:<port>` with no `Authorization` bearer
header. `curl --resolve testserver:<port>:127.0.0.1 ...` reproduces the request
shape a victim browser would produce when the victim's resolver maps
`testserver` → `127.0.0.1`.

**Code path (fixed v0.1.10):**
1. `_reject_untrusted_loopback_host` (`api_server.py:562`): `_is_local_client()`
   True; `_is_allowed_loopback_host("testserver:18899")` →
   `_host_without_port` → `"testserver"` ∈ `_DEFAULT_LOOPBACK_HOSTS` → **True**;
   middleware **passes** (no 403).
2. `require_auth` → `_validate_api_auth` (`api_server.py:760`):
   `_is_local_client()` True → **returns immediately (no auth)**.
3. `POST /sessions` (`api_server.py:1801`) → HTTP 201, session created.
4. `POST /sessions/{id}/messages` (`api_server.py:2094`) → agent loop.
5. `_shell_tools_enabled_for_request` (`api_server.py:844`): returns
   `_env_shell_tools_enabled()`. Default: **False** (bash disabled). With
   `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`: **True** → `bash` tool registered.
6. Mock/attacker LLM returns a `bash` tool-call → `BashTool.execute`
   (`agent/src/tools/bash_tool.py`) → `subprocess.run(command, shell=True)` →
   RCE.

**Other candidates tested and ruled out:**
- **Attacker-domain Host** (`rebind.attacker.test`): fixed v0.1.10 → 403 (fix
  works). This is the control, not a bypass.
- **Direct cross-origin fetch to `localhost`**: `Host: localhost` is allowlisted
  by design; cross-origin browser requests are CORS-limited (loopback-only
  origins, no `*` with credentials), so the session_id cannot be read to drive
  RCE. Not a bypass.
- **Unauthenticated routes** (`/health`, `/api`, `/skills`, `/correlation`):
  non-sensitive / read-only; do not reach the auth-bypass or RCE sink.
- **WebSocket / SSE transports**: no `@app.websocket` endpoints; SSE streams go
  through the Host middleware then `_validate_api_auth`.
- **Non-shell file tools** (`write_file`, `edit_file`): not gated by the
  shell-tools opt-in, so they could overwrite `agent/.env` once an auth bypass
  exists — a secondary gap, not an independent bypass.

## Impact

- **Package/component affected:** `HKUDS/Vibe-Trading` —
  `agent/api_server.py` (`_DEFAULT_LOOPBACK_HOSTS` / `_reject_untrusted_loopback_host`
  allowlist), `agent/src/tools/bash_tool.py` (BashTool RCE primitive),
  `agent/src/tools/__init__.py` (`_SHELL_TOOL_NAMES` gating).
- **Affected versions (as tested):**
  - Vulnerable baseline: v0.1.9, commit `8cebccb9b301f962d11bf0fa8c6be9bf7d7e12f2`.
  - Fixed target under test: v0.1.10, commit `54f944743efee7b9cbaaeafb5966c583d0d4ac66`.
- **Risk level:** High for the conditional full-bypass scenario (opted-in shell
  tools + `testserver` resolves to loopback): unauthenticated RCE as the API
  process user. Medium for the default-config partial bypass: unauthenticated
  session creation / control-plane access without RCE.
- **Consequences:** unauthenticated session creation and agent control on the
  fixed version via `Host: testserver`; full RCE when shell tools are opted in.

## Impact Parity

- **Disclosed/claimed maximum impact (parent CVE):** DNS rebinding →
  authentication bypass → remote code execution (code_execution) and credential
  exfiltration.
- **Reproduced impact from this variant run:**
  - Auth bypass on fixed v0.1.10 via `Host: testserver` → HTTP 201 (confirmed).
  - Full RCE on fixed v0.1.10 **only** in the opted-in shell-tools config
    (`VIBE_TRADING_ENABLE_SHELL_TOOLS=1`): `BashTool` executed
    `id; whoami; echo RCE_CONFIRMED > /tmp/...`; proof file written
    (`RCE_CONFIRMED`); run `state.json` = `{"status":"success"}`.
  - In the default fixed config, the auth bypass does **not** escalate to RCE
    (bash gated by PR #243).
- **Parity:** `partial`. The auth-bypass half of the CVE is reproduced on the
  fixed version; the RCE half is reproduced only under the non-default
  opted-in-shell-tools condition, and the standard DNS-rebinding delivery of
  `Host: testserver` is not confirmed.
- **Not demonstrated:** a fully unattended, standard-DNS-rebinding RCE bypass on
  the fixed version (the attacker cannot deliver `Host: testserver` from an
  attacker-owned domain), and credential exfiltration via settings writes on the
  fixed version (PR #245 blocks it).

## Root Cause

The same underlying bug — **loopback peer-IP trust accepted without a
sufficiently strict Host validation** — is still reachable because the fix's
allowlist (`_DEFAULT_LOOPBACK_HOSTS`) over-trusts a non-loopback name,
`"testserver"`. The fix's middleware correctly rejects attacker-domain Hosts,
but the allowlist membership test `normalized in _DEFAULT_LOOPBACK_HOSTS`
returns True for `testserver`, so a loopback peer bearing that Host is treated
as a trusted local caller and skips `_validate_api_auth`. The BashTool sink
(`subprocess.run(shell=True)`) is then reachable when shell tools are opted in.

Fix commit (the Host middleware): PR #242, commit `6e06017`
(`6e06017...` in `v0.1.9..v0.1.10`). The gap lives in the
`_DEFAULT_LOOPBACK_HOSTS` frozenset at `agent/api_server.py:483`.

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh` — self-contained.
   Run with `PRUVA_ROOT=<bundle_dir> bash bundle/vuln_variant/reproduction_steps.sh`.
2. **What the script does:**
   - Ensures Python deps; creates/uses isolated git worktrees
     (`bundle/artifacts/vt-vuln` @ v0.1.9, `bundle/artifacts/vt-fixed` @ v0.1.10)
     so the repro cache clone is **not** mutated.
   - Starts a mock OpenAI-compatible LLM that returns a `bash` tool-call.
   - **Phase 1 (v0.1.9 baseline):** for Hosts `rebind.attacker.test`,
     `testserver`, `localhost` → `POST /sessions` (all 201, no Host check);
     full RCE chain via attacker Host (control).
   - **Phase 2 (v0.1.10 default):** attacker Host → 403 (fix works);
     `testserver` Host → **201 (partial bypass)**; `localhost` → 201 (baseline);
     `testserver` + message → **no RCE** (bash gated by opt-in).
   - **Phase 3 (v0.1.10 opted-in, `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`):**
     attacker Host → 403 (Host middleware still blocks DNS rebinding);
     `testserver` Host → 201 → message → **BashTool RCE CONFIRMED**.
   - Logs every attempt; exits 1 (no standard DNS-rebinding RCE bypass) but
     records the partial/conditional bypass.
3. **Expected evidence:**
   - `logs/vuln_variant/fixed_default_host_testserver.txt` → HTTP 201 (auth
     bypass on fixed).
   - `logs/vuln_variant/fixed_default_host_rebind.attacker.test.txt` → HTTP 403
     (fix blocks standard DNS rebinding).
   - `logs/vuln_variant/fixed_optedin_host_testserver.txt` → HTTP 201.
   - `logs/vuln_variant/fixed_optedin_ts_rce_proof.txt` → `RCE_CONFIRMED`
     (full chain on fixed, opted-in config).
   - `logs/vuln_variant/fixed_optedin_ts_state.json` → `{"status":"success"}`.
   - `logs/vuln_variant/vuln_rce_proof.txt` → `RCE_CONFIRMED` (v0.1.9 control).

## Evidence

All artifacts under `bundle/logs/vuln_variant/` and `bundle/vuln_variant/`:

| Artifact | Description |
|---|---|
| `logs/vuln_variant/variant_steps.log` | Full script execution log (both runs). |
| `logs/vuln_variant/vuln_host_{attacker,testserver,localhost}.txt` | v0.1.9: all Hosts → 201 (no Host check). |
| `logs/vuln_variant/vuln_rce_proof.txt` | v0.1.9 control RCE: `RCE_CONFIRMED`. |
| `logs/vuln_variant/fixed_default_host_rebind.attacker.test.txt` | Fixed default: attacker Host → **403**. |
| `logs/vuln_variant/fixed_default_host_testserver.txt` | Fixed default: `testserver` Host → **201** (partial bypass). |
| `logs/vuln_variant/fixed_default_host_localhost.txt` | Fixed default: localhost → 201 (baseline). |
| `logs/vuln_variant/fixed_default_ts_msg.txt` | Fixed default: testserver message → no RCE (bash gated). |
| `logs/vuln_variant/fixed_optedin_host_rebind.attacker.test.txt` | Fixed opted-in: attacker Host → **403**. |
| `logs/vuln_variant/fixed_optedin_host_testserver.txt` | Fixed opted-in: `testserver` Host → **201**. |
| `logs/vuln_variant/fixed_optedin_ts_msg.txt` | Fixed opted-in: testserver message → triggers bash. |
| `logs/vuln_variant/fixed_optedin_ts_rce_proof.txt` | Fixed opted-in: **RCE_CONFIRMED** (full chain on fixed). |
| `logs/vuln_variant/fixed_optedin_ts_state.json` | Run state: `{"status":"success"}`. |
| `logs/vuln_variant/api_server_{vuln,fixed-default,fixed-optedin}.log` | API server logs per phase. |
| `vuln_variant/mock_llm_server.py` | Mock OpenAI LLM returning a bash tool-call. |
| `vuln_variant/reproduction_steps.sh` | Variant reproduction script. |

**Key excerpt — fixed v0.1.10 default, `testserver` Host:**
```
Host=testserver
HTTP 201
{"session_id":"0d6dac8dad41","title":"","status":"active",...}
```
**Key excerpt — fixed v0.1.10 default, attacker Host (control):**
```
Host=rebind.attacker.test
HTTP 403
{"detail":"Untrusted local API host"}
```
**Key excerpt — fixed v0.1.10 opted-in RCE:**
```
[08:29:23]   *** fixed-optedin testserver RCE CONFIRMED: RCE_CONFIRMED ***
state.json: {"status":"success"}
```

**Environment:** Python 3.14.4, FastAPI + uvicorn, langchain-openai; isolated
git worktrees at v0.1.9 (`8cebccb`) and v0.1.10 (`54f9447`); API bound to
`0.0.0.0:18899` (dev mode, no `API_AUTH_KEY`); mock LLM on `127.0.0.1:19099`.

## Recommendations / Next Steps

1. **Remove `testserver` from the production allowlist.** Move it to a
   test-only override (e.g. an env flag `VIBE_TRADING_TEST_HOSTS` consulted only
   when testing, or inject it in `conftest.py` via `API_ALLOWED_HOSTS` for the
   TestClient). Production `_DEFAULT_LOOPBACK_HOSTS` should contain only genuine
   loopback names: `localhost`, `127.0.0.1`, `::1`, `[::1]`. This closes the
   partial bypass with **no production behavior change** (no real client sends
   `Host: testserver`).
2. **Tighten the allowlist semantics.** Consider validating that the Host, after
   port stripping, resolves to a loopback address (or is an explicit operator
   entry) rather than trusting fixed bare labels — defense in depth against
   future allowlist drift.
3. **Gate file-mutating agent tools** (`write_file`, `edit_file`,
   `skill_writer`) behind the same `VIBE_TRADING_ENABLE_SHELL_TOOLS` opt-in (or
   a separate `VIBE_TRADING_ENABLE_FILE_TOOLS`), so an auth-bypassed agent
   cannot overwrite `agent/.env` to redirect credentials, bypassing PR #245's
   settings-write gate.
4. **Keep the shell-tools opt-in default-off** (PR #243) — it correctly prevents
   the `testserver` partial bypass from escalating to RCE in the default
   configuration. Document that operators who enable shell tools must not expose
   the API to environments where `testserver` resolves to loopback.
5. **Bind defaults:** consider changing the default `--host` from `0.0.0.0` to
   `127.0.0.1` so a fresh dev deployment is not network-reachable by default.

## Additional Notes

- **Idempotency:** the script was run twice; both runs completed without
  crashing and produced identical verdicts (exit 1; partial auth-bypass = yes;
  opted-in RCE = yes; standard DNS-rebinding bypass = no). It reuses existing
  worktrees, frees ports between phases, and cleans up on exit.
- **No repo state mutation:** the repro cache clone
  (`/data/pruva/project-cache/.../repo`) remained at v0.1.10
  (`54f9447`) throughout; all version switching used isolated git worktrees
  under `bundle/artifacts/`.
- **Honest scope:** this is reported as a **partial/conditional bypass**, not a
  confirmed standard DNS-rebinding RCE bypass, because `Host: testserver` is not
  deliverable from an attacker-owned domain via normal DNS rebinding. The
  finding is nonetheless a real fix-completeness defect (an over-broad
  production allowlist) that restores the full chain under a realistic
  operator-opted-in configuration.
