Vulnerability GHSA-v2f8-6655-7grj

Critical
CRITICAL RISK
CVSS Score: 10.0
Score Range: 9.0–10.0
Critical severity vulnerabilities (CVSS 9.0–10.0). These represent the highest impact issues.
18 days ago
October 02, 2026 at 10:44 PM UTC
Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain
0.1.0 - 0.1.6
0.1.0 - 0.1.6

Summary

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

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).

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 3 — High: Unauthenticated POST /upload writes arbitrary .py/.sh/.yaml files to server filesystem with full path returned

  • Severity: High
  • CVSS v3.1: 8.1 — AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N
  • CVSS v4.0: 8.7 — AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:H/SI:H/SA:N
  • CWE: CWE-434 (Unrestricted Upload of File with Dangerous Type)

Affected file:

  • agent/api_server.py:1310-1321 — _BLOCKED_UPLOAD_EXT set rejects .exe, .msi, .bat, .cmd, .com, .scr, .app, .dmg, .so, .dll, .dylib, .zip, .rar, .7z, .tar, .gz, .tgz, .bz2, .xz — but not .py, .sh, .yaml, .j2, .json, .html, or Dockerfile
  • agent/api_server.py:1347 — @app.post("/upload", dependencies=[Depends(require_auth)]) (no-op when API_AUTH_KEY unset)

Intent vs actual: POST /upload is intended as an authenticated file-staging mechanism. In default config the auth dependency is a no-op (per F1). The extension blocklist is a denylist not an allowlist, so dangerous executable-adjacent types pass through. The uploaded file is saved as <uuid>.<ext> and the full resolved server path is returned in the response body under "file_path", so the attacker does not need to guess paths.

Steps to observe (no LLM key required):

  1. Start the server in default config (no API_AUTH_KEY).
  2. curl -s -X POST http://HOST:8899/upload -F 'file=@/dev/stdin;filename=payload.py' <<< 'print("ATTACKER_CONTROLLED")'
  3. Observe HTTP 200 with JSON containing "status":"ok" and "file_path":"/app/agent/uploads/<uuid>.py".
  4. Repeat with filename=config.yaml and filename=run.sh to confirm multiple executable-adjacent types pass.

Impact: Any unauthenticated caller can write arbitrary Python scripts, shell scripts, or YAML configuration to a known, server-returned path. F3 is independent of any LLM API key — it is the cleanest unauth-write primitive in the codebase. The uploaded file is reachable from the LLM agent's tool envelope and chains into Group B / F8 (backtest exec_module premature exec) for a non-bash RCE path.

Finding A5 — Medium: Settings endpoints expose first-4 + last-4 characters of every configured API key via _mask_secret()

  • Severity: Medium
  • CVSS v3.1: 5.3 — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
  • CVSS v4.0: 6.9 — AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
  • CWE: CWE-200 (Exposure of Sensitive Information)

Affected files:

  • agent/api_server.py:467-474 — _mask_secret() returns f"{value[:4]}...{value[-4:]}" for any string longer than 8 characters
  • agent/api_server.py:372 — LLM_API_KEY_PLACEHOLDERS filters out shipped placeholders so the leak fires only when a real key is configured
  • agent/api_server.py:505-522 — GET /settings/llm returns api_key_hint = _mask_secret(api_key) if api_key_configured else None
  • agent/api_server.py:561 — GET /settings/data-sources returns tushare_token_hint=_mask_secret(token) if token_configured else None
  • agent/test_settings_api.py:106 — assertion api_key_hint == "or-s...alue" for input or-secret-value confirms the 4+4 reveal is the intended API contract

Intent vs actual: The frontend needs to confirm to the operator that an API key is configured, so the appropriate hint is a boolean presence indicator or a fixed placeholder. The actual implementation reveals the first 4 and last 4 characters. For structured provider keys with predictable prefixes (sk-or-v1-, gsk_, xoxb-, ts-), the leading 4 bytes are largely fixed and the entropy leak is concentrated in the trailing 4 — which can be material for tokens with bounded total entropy (e.g. Tushare tokens). A runtime probe configured OPENROUTER_API_KEY=sk-or-v1-AbCdEfGhXyZ12345fakekey and TUSHARE_TOKEN=ts-real-shaped-token-1234567890ab and observed api_key_hint='sk-o...ekey' and tushare_token_hint='ts-r...90ab' from the corresponding GET endpoints.

Steps to observe (no LLM key required other than the configured value being non-placeholder; the call itself does not consume the LLM):

  1. Edit agent/.env to replace the placeholder with a real-shaped key, e.g. OPENROUTER_API_KEY=sk-or-v1-TestKeyAbcde12345. Restart the server.
  2. From any loopback client or via the F-A4 cross-origin path, curl -s "http://127.0.0.1:8899/settings/llm" (no Authorization header).
  3. Observe HTTP 200 with api_key_configured=true and api_key_hint containing the first 4 and last 4 characters of the real key.
  4. curl -s "http://127.0.0.1:8899/settings/data-sources" to observe tushare_token_hint follow the same pattern.

Impact: Every functional deployment leaks structured bytes of the configured API keys. Combined with offline guessing against bounded-entropy provider keys (notably Tushare tokens), the partial reveal can narrow the attack space to a tractable bruteforce window. The leak is reachable from the F-A4 cross-origin browser path, so it does not require even loopback TCP access — only an operator who visits a malicious local web page in a browser running on the same host as the agent.

Impacted packages

Timeline

Published
18 days ago
October 02, 2026 at 10:44 PM UTC
Fixed (0.1.7)
5 months ago
May 06, 2026 at 12:09 PM UTC
Last Modified
1 hour ago
October 21, 2026 at 10:57 AM UTC