Vibe-Trading file-read tools expose arbitrary server-readable files
📋 Description
Summary:
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.
Shared baseline (applies to both findings)
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.
Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials
- 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-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)
Affected file: agent/src/tools/path_utils.py
- line 52 —
def safe_user_path(p: str) -> Path: - line 73-77 —
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_rsaand any other SSH key files/root/.aws/credentials,/root/.kube/config,/root/.docker/config.json/app/agent/.env(the file containing the operator's realOPENROUTER_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:
- Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.
- (Setup for safe demo: inside the container,
docker execa 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."}'- Poll
curl -s "http://HOST:8899/sessions/$SID/messages". Observe the agent invokeExtractShadowStrategyToolwithjournal_path="/root/secrets/aws.csv", which passessafe_user_path()and attempts to parse the file as a trade journal CSV. - Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.
- Repeat with
journal_path="/app/agent/.env"to confirm the.envfile 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.
Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ
- 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-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)
Affected file: agent/src/tools/doc_reader_tool.py
- line 259 —
def read_document(file_path: str, pages: str = "") -> str: - line 270 —
path = Path(file_path)— followed only bypath.exists()andpath.is_file()checks before dispatching to format-specific readers - No call to
safe_path,safe_user_path, or any other sandbox enforcement appears anywhere in the function
Intent 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, includingOPENROUTER_API_KEYandTUSHARE_TOKENin plaintext
This 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:
- Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.
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"}'- Poll
curl -s "http://HOST:8899/sessions/$SID/messages". Observe the agent invokeread_documentwithfile_path="/proc/self/environ"and return the full process environment in the message stream. - Observe
OPENROUTER_API_KEY,TUSHARE_TOKEN, and any other variables inagent/.envappearing in plaintext. - Repeat with
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.
Why F9 and F10 are listed separately
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)inread_documentbefore line 270). F9's fix is insafe_user_path()itself: the envelope must be replaced with a strict allowlist of operator-configured directories, notPath.home() ∪ Path.cwd(). A fix that adds the missingsafe_user_pathcall toread_documentis insufficient becausesafe_user_pathitself accepts/rootand/app/agent/.env. Both surfaces need work. -
Different reachability classes — F9 is reachable through tools that already gate on
safe_user_path(ExtractShadowStrategyTooland several journal tools), so even a hypothetical F10 fix that switchedread_documentto usesafe_user_pathwould still leak/root/*because the envelope is broken. F9 is the structural defect; F10 is the missed call.
Suggested remediation
-
F9 — In
safe_user_path()atpath_utils.py:52-77, replace thePath.home() ∪ Path.cwd()envelope with a strict allowlist of operator-configured directories (e.g. an explicitBROKER_EXPORTS_DIRenv 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()(orsafe_user_path()) call atdoc_reader_tool.py:270before the existingpath.exists()/path.is_file()checks. Once F9 is patched, the same allowlist will apply uniformly to bothread_documentand thesafe_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 /appstep to the Dockerfile andUSER vibebeforeCMD. 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 matchingUSERrecommendation.
🎯 Affected products1
- pip/vibe-trading-ai:>= 0.1.0, < 0.1.7