GHSA-5rmq-chc7-m22fHigh· 7.5▾ TwilightVibe-Trading file-read tools expose arbitrary server-readable files
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 41.3 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
2 findings — safe_user_path() accepts any path under Path.home() or Path.cwd(), which inside the shipped root container resolves to /root and /app (so all of root's home, including /root/.ssh/id_rsa, /root/.aws/credentials, /root/.kube/config, and /app/agent/.env, passes the check) (F9). read_document() has no sandbox call at all and returns the full content of any path the FastAPI process can read, including /etc/shadow, /etc/passwd, /proc/self/environ, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing safe_path() call in one function; F9 = the envelope definition in path_utils.py), so both must be patched.
The container has no USER directive (Dockerfile:15 — FROM python:3.11-slim AS runtime, no subsequent USER), so the FastAPI process runs as uid=0(root).
The two file-read tools described here are members of the auto-discovered LLM tool registry.
Combined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes.
See GHSA-1's shared reproducer block for the install steps; the same docker compose up -d setup applies here.
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.
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NAV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NAffected file: agent/src/tools/path_utils.py
def safe_user_path(p: str) -> Path:if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved — home = Path.home(), cwd = Path.cwd()Intent vs actual:
safe_user_path() is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside Path.home() or Path.cwd(). Inside the shipped Docker container, Path.home() resolves to /root and Path.cwd() resolves to /app. Every file under either subtree passes the check, including:
/root/.ssh/id_rsa and any other SSH key files/root/.aws/credentials, /root/.kube/config, /root/.docker/config.json/app/agent/.env (the file containing the operator's real OPENROUTER_API_KEY, TUSHARE_TOKEN, and any other secrets)A runtime probe inside the container confirmed that safe_user_path('/root/.aws/credentials') returned the path without raising ValueError. ExtractShadowStrategyTool was then invoked against /root/secrets/aws.csv (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.
Steps to observe:
docker exec a planted file: docker exec <container> sh -c 'mkdir -p /root/secrets && printf "broker_id,api_key,api_secret\nDEMO,FAKE_KEY,FAKE_SECRET\n" > /root/secrets/aws.csv'.)curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Content-Type: application/json' -d '{"content":"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find."}'curl -s "http://HOST:8899/sessions/$SID/messages". Observe the agent invoke ExtractShadowStrategyTool with journal_path="/root/secrets/aws.csv", which passes safe_user_path() and attempts to parse the file as a trade journal CSV.journal_path="/app/agent/.env" to confirm the .env file is within the accepted envelope.Impact:
Any unauthenticated caller can instruct the LLM to attempt to parse any file under /root or /app as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, /root encompasses all credentials a careless operator may have mounted into the home directory; /app includes the agent's own secrets and any operator-staged data files.
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NAV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NAffected file: agent/src/tools/doc_reader_tool.py
def read_document(file_path: str, pages: str = "") -> str:path = Path(file_path) — followed only by path.exists() and path.is_file() checks before dispatching to format-specific readerssafe_path, safe_user_path, or any other sandbox enforcement appears anywhere in the functionIntent vs actual: DocReaderTool is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted file_path string, runs only path.exists() and path.is_file(), and dispatches to the appropriate reader. No call to safe_path or safe_user_path exists in the function. A runtime probe confirmed:
read_document('/etc/passwd') returned HTTP 200 with 839 characters of contentread_document('/etc/shadow') returned the full shadow password fileread_document('/proc/self/environ') returned the full process environment, including OPENROUTER_API_KEY and TUSHARE_TOKEN in plaintextThis is strictly wider than F9: F9 is bounded to /root + /app via the (overly-broad) envelope; F10 has no envelope at all and reaches /etc, /proc, /var, and any other path the FastAPI process can read.
Steps to observe:
curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Content-Type: application/json' -d '{"content":"Please read and summarize the document at /proc/self/environ"}'curl -s "http://HOST:8899/sessions/$SID/messages". Observe the agent invoke read_document with file_path="/proc/self/environ" and return the full process environment in the message stream.OPENROUTER_API_KEY, TUSHARE_TOKEN, and any other variables in agent/.env appearing in plaintext.file_path="/etc/shadow" to confirm shadow password file access.Impact:
An unauthenticated caller can retrieve any file the server process can read. Running as root, that includes /etc/shadow, /etc/passwd, /proc/self/environ (full plaintext API keys), /root/.ssh/id_rsa, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does not require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.
A maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:
Different fix scope — F10's fix is a single missing call (safe_path(file_path) in read_document before line 270). F9's fix is in safe_user_path() itself: the envelope must be replaced with a strict allowlist of operator-configured directories, not Path.home() ∪ Path.cwd(). A fix that adds the missing safe_user_path call to read_document is insufficient because safe_user_path itself accepts /root and /app/agent/.env. Both surfaces need work.
Different reachability classes — F9 is reachable through tools that already gate on safe_user_path (ExtractShadowStrategyTool and several journal tools), so even a hypothetical F10 fix that switched read_document to use safe_user_path would still leak /root/* because the envelope is broken. F9 is the structural defect; F10 is the missed call.
F9 — In safe_user_path() at path_utils.py:52-77, replace the Path.home() ∪ Path.cwd() envelope with a strict allowlist of operator-configured directories (e.g. an explicit BROKER_EXPORTS_DIR env var defaulting to /app/data/broker_exports/). Reject /root, /app/agent/.env, and /app/agent/uploads/ (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'
F10 — Add a safe_path() (or safe_user_path()) call at doc_reader_tool.py:270 before the existing path.exists() / path.is_file() checks. Once F9 is patched, the same allowlist will apply uniformly to both read_document and the safe_user_path-gated tools.
Defense-in-depth — Drop the FastAPI process to a non-root user. Add a RUN useradd -m vibe && chown -R vibe /app step to the Dockerfile and USER vibe before CMD. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching USER recommendation.
vibe-trading-ai >= 0.1.0, < 0.1.7Upgrade to a patched release:
vibe-trading-ai 0.1.7Connected by shared product, vendor, weakness, or advisory.
GHSA-v2f8-6655-7grjCritical· 10.0Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain
GHSA-jqmf-mx4f-hfr6Critical· 10.0Vibe-Trading LLM-callable tools permit command execution, code injection, and SSRF
CVE-2025-69226Medium· 5.3AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python
CVE-2025-58752Medium· 5.3Vite is a frontend tooling framework for JavaScript
CVE-2022-31746Medium· 6.5Internal URLs are protected by a secret UUID key, which could have been leaked to web page through the Referrer header
CVE-2026-11769MediumGrafana Operator: Privilege escalation from namespace admin to cluster admin via GrafanaDashboard jsonnetLib fileName