Summary
Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain
Advisory details
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)
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
HOSTplaceholder used throughout the per-finding "Steps to observe" blocks below: replaceHOSTwith the address you reach the docker host on — typicallylocalhost(or127.0.0.1) if you are running the reproducer on the same machine as the container. Allcurlcommands 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: returnearly return inrequire_auth()agent/api_server.py:736—@app.post("/sessions/{session_id}/messages", dependencies=[Depends(require_auth)])(the dependency is a no-op whenAPI_AUTH_KEYis unset)agent/src/tools/bash_tool.py:44-46—subprocess.run(command, shell=True, cwd=cwd)with command read fromkwargs['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:
- Start the server per the shared reproducer above (with a working
OPENROUTER_API_KEYset inagent/.env). Confirmcurl -fs http://HOST:8899/healthreturns 200. 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'])")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."}'- Wait ~5–15 seconds, then
curl -s "http://HOST:8899/sessions/$SID/messages"and observe the BashTool result message containinguid=0(root), the kernel version, and the container hostname — all returned with noAuthorizationheader 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:
- Set
API_AUTH_KEY=any-secret-valueinagent/.env. Under docker compose, add a bind-mount on thevibe-tradingservice so the container reads the change:volumes: - ./agent/.env:/app/agent/.env:ro. Thendocker compose up -d --force-recreate(a plainrestartreuses the existing process env and will not pick up the change). 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'])")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)- No-auth read —
curl -s "http://HOST:8899/sessions/$SID/messages"(noAuthorizationheader). Observe HTTP 200 with the broker token visible. - 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 broke
References
Related vulnerabilities
All Supply chain →- CRITICALCVE-2026-102748
Dgraph: Zero Unauthenticated HTTP Admin removeNode Denial of Service
- MEDIUMGHSA-3hc7-r24j-rpwc
Vikunja: Cross-project task disclosure through subtask expansion
- HIGHCVE-2026-91985
Vikunja: Read-only project members can obtain any link share's access hash via the single-share read endpoint (v1 and v2) and escalate to the share's permission level
- HIGHCVE-2026-107811
0xJacky/nginx-ui /api/nodes Leaks Cluster Node Tokens and Allows Cross-Node Impersonation as initUser
- HIGHCVE-2026-61426
PraisonAI: AgentOS defaults to network-exposed no-auth mode, allowing unauthenticated agent invocation and instruction disclosure
- HIGHCVE-2026-61427
PraisonAI: MCP HTTP-stream transport is unauthenticated by default, exposing tool enumeration and an unvalidated tool-call surface