GitHub Advisoryadvisory · PyPI
GHSA-v2f8-6655-7grj Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain
CRITICAL
Official page ↗
- Ecosystem
- PyPI
- Packages
- vibe-trading-ai
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-200, CWE-306, CWE-434, CWE-862, CWE-942
- Reviewed
- 2026-10-02
Affected packages
• PyPI vibe-trading-ai: >= 0.1.0, < 0.1.7 → fixed in 0.1.7
Description
Summary:
5 findings — unauthenticated full-API exposure (F1, lead Critical), read-side authorization gap that persists even with `API_AUTH_KEY` set (F2), unauthenticated file write of `.py`/`.sh`/`.yaml` to a server-returned path (F3), default-permissive CORS that combines with a loopback-only check to grant any browser page on whitelisted localhost ports credentialed cross-origin access (F-A4), and partial API-key disclosure via `_mask_secret()` (F-A5).
---
Shared baseline (applies to all 5 findings)
The shipped `agent/.env.example` line 112 ships `# API_AUTH_KEY=` commented out. `require_auth()` at `agent/api_server.py` line 303 executes `if not api_key: return` and returns `None` immediately when `API_AUTH_KEY` is unset, so every endpoint decorated with `dependencies=[Depends(require_auth)]` operates as unauthenticated. The shipped `Dockerfile` does not contain a `USER` directive, so the FastAPI process runs as `uid=0(root)` inside the container (verified: `docker exec id` returns `uid=0(root) gid=0(root)`). The `docker-compose.yml` binds `0.0.0.0:8899` with no network restriction.
The only operator action required beyond a clean install is supplying a working LLM API key so the agent loop can complete its tool-call round trip — this is the normal first step to make the agent functional, not an additional security opt-in. F-A4 and F-A5 do *not* require an LLM key (see per-finding notes); F1 and F3 do not require an LLM key for the unauth surface itself, only for the chained RCE demonstration in F1.
Reproducer environment (common)
```sh
git clone https://github.com/HKUDS/Vibe-Trading.git
cd Vibe-Trading
git checkout 7452610113a75529b5d55fd2217bb17f7bec66f7 # v0.1.6 + 1 frontend fix; same vuln state as v0.1.6
cp agent/.env.example agent/.env
(For F1 chained demo only:) edit agent/.env to set OPENROUTER_API_KEY=
docker compose up -d
port 8899 is now reachable; HOST below is the docker host's IP from the attacker's perspective
```
> Note on the `HOST` placeholder used throughout the per-finding "Steps to observe" blocks below: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.
---
Finding 1 — Critical: Unauthenticated network client reaches shell execution via POST /sessions/{id}/messages
• Severity: Critical
• CVSS v3.1: 9.8 — `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`
• CVSS v4.0: 10.0 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H`
• CWE: CWE-306 (Missing Authentication for Critical Function); chains into CWE-78 (Group B, F6)
Affected files:
• `agent/api_server.py:303` — `if not api_key: return` early return in `require_auth()`
• `agent/api_server.py:736` — `@app.post("/sessions/{session_id}/messages", dependencies=[Depends(require_auth)])` (the dependency is a no-op when `API_AUTH_KEY` is unset)
• `agent/src/tools/bash_tool.py:44-46` — `subprocess.run(command, shell=True, cwd=cwd)` with command read from `kwargs['command']` (full bug detail tracked in GHSA-2 / Group B / F6)
Intent vs actual: The session API is intended to serve authenticated users only. When `API_AUTH_KEY` is unset, `require_auth()` returns `None` immediately and `dependencies=[Depends(require_auth)]` becomes a no-op. Any anonymous TCP client to port 8899 can therefore create a session, post a message, and receive the LLM agent's response. The LLM ReAct agent — given a natural-language request to run a command — selects `BashTool` from the auto-discovered registry, which calls `subprocess.run(command, shell=True)` with the LLM-emitted string. The container has no `USER` directive, so the resulting process runs as `uid=0(root)`.
Steps to observe:
1. Start the server per the shared reproducer above (with a working `OPENROUTER_API_KEY` set in `agent/.env`). Confirm `curl -fs http://HOST:8899/health` returns 200.
2. `SID=$(curl -s -X POST http://HOST:8899/sessions -H 'Content-Type: application/json' -d '{}' | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])")`
3. `curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Content-Type: application/json' -d '{"content":"Execute the shell command '\''id && uname -a'\'' and report the output verbatim."}'`
4. Wait ~5–15 seconds, then `curl -s "http://HOST:8899/sessions/$SID/messages"` and observe the BashTool result message containing `uid=0(root)`, the kernel version, and the container hostname — all returned with no `Authorization` header on any of the three requests.
Impact: An unauthenticated caller with TCP access to port 8899 can execute arbitrary shell commands as root inside the container. This is the top-severity entry point of the RCE chain. Combined with the absent `USER` directive in the Dockerfile, the blast radius is full container takeover.
---
Finding 2 — High: Read endpoints return full session history with no authentication, even when API_AUTH_KEY is set
• Severity: High
• CVSS v3.1: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`
• CVSS v4.0: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`
• CWE: CWE-862 (Missing Authorization)
Affected file: `agent/api_server.py` — `require_auth()` docstring at line 289 states "Only write endpoints (POST/PUT/DELETE/PATCH) use this dependency." Read endpoints with no `Depends(require_auth)`:
• line 804 — `@app.get("/runs", response_model=List[RunInfo])`
• line 788 — `@app.get("/runs/{run_id}", response_model=RunResponse)`
• line 748 — `@app.get("/runs/{run_id}/code")`
• line 769 — `@app.get("/runs/{run_id}/pine")`
• line 1153 — `@app.get("/sessions", response_model=List[SessionResponse])`
• line 1173 — `@app.get("/sessions/{session_id}", response_model=SessionResponse)`
• line 1251 — `@app.get("/sessions/{session_id}/messages", response_model=List[MessageResponse])`
• line 1272 — `@app.get("/sessions/{session_id}/events")`
• line 1444 — `@app.get("/swarm/runs")`
Intent vs actual: When `API_AUTH_KEY` is configured, the implicit operator expectation is that all session data is protected. The actual design is documented in the docstring at line 289 — read endpoints have no `Depends(require_auth)`, so they remain unauthenticated even with `API_AUTH_KEY` set. A runtime probe with `API_AUTH_KEY=any-secret-value` configured: a session was created with a valid Bearer token, a message containing `BROKER_TOKEN=ts-secret-deadbeef-real-private-data` was posted, then `GET /sessions/{id}/messages` was issued with no `Authorization` header and returned HTTP 200 with the broker token string verbatim in the response — confirming the gap persists when authentication is enabled.
Steps to observe:
1. Set `API_AUTH_KEY=any-secret-value` in `agent/.env`. Under docker compose, add a bind-mount on the `vibe-trading` service so the container reads the change: `volumes: - ./agent/.env:/app/agent/.env:ro`. Then `docker compose up -d --force-recreate` (a plain `restart` reuses the existing process env and will not pick up the change).
2. `SID=$(curl -s -X POST http://HOST:8899/sessions -H 'Authorization: Bearer any-secret-value' -H 'Content-Type: application/json' -d '{}' | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])")`
3. `curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Authorization: Bearer any-secret-value' -H 'Content-Type: application/json' -d '{"content":"BROKER_TOKEN=ts-secret-test-value"}'` (this requires the Bearer token because POST is auth-protected)
4. No-auth read — `curl -s "http://HOST:8899/sessions/$SID/messages"` (no `Authorization` header). Observe HTTP 200 with the broker token visible.
5. Also: `curl -s "http://HOST:8899/runs"` returns all run records (including their prompt fields) with no auth.
Impact: An unauthenticated caller can enumerate the full history of every agent session — including any broker tokens, LLM API keys, …
Related
Weakness · CWE (5)
- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor weakness
- CWE-306 Missing Authentication for Critical Functionality weakness
- CWE-434 Unrestricted Upload of File with Dangerous Type weakness
- CWE-862 Missing Authorization weakness
- CWE-942 Permissive Cross-domain Security Policy with Untrusted Domains weakness