GHSA-5rmq-chc7-m22f: Path Traversal
Summary: 2 findings — safeuserpath() 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/idrsa, /root/.aws/credentials, /root/.kube/config, and /app/agent/.env, passes the check) (F9). readdocument() 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 safepath() call in one function; F9 = the envelope definition in pathutils.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 HOST placeholder used throughout the per-finding "Steps to observe" blocks below: replace HOST with the address you reach the docker host on — typically localhost (or 127.0.0.1) if you are running the reproducer on the same machine as the container. All curl commands below assume this substitution.
---
Finding 9 — High: safeuserpath() 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/pathutils.py - line 52 — def safeuserpath(p: str) -> Path: - line 73-77 — if resolved.isrelativeto(home) or resolved.isrelativeto(cwd): return resolved — home = Path.home(), cwd = Path.cwd()
Intent vs actual: safeuserpath() 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/idrsa 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 OPENROUTERAPIKEY, TUSHARETOKEN, and any other secrets)
A runtime probe inside the container confirmed that safeuserpath('/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:
1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session. 2. (Setup for safe demo: inside the container, docker exec a planted file: docker exec <container> sh -c 'mkdir -p /root/secrets && printf "brokerid,apikey,apisecret\nDEMO,FAKEKEY,FAKESECRET\n" > /root/secrets/aws.csv'.) 3. 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."}' 4. Poll curl -s "http://HOST:8899/sessions/$SID/messages". Observe the agent invoke ExtractShadowStrategyTool with journalpath="/root/secrets/aws.csv", which passes safeuserpath() and attempts to parse the file as a trade journal CSV. 5. 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. 6. Repeat with journalpath="/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.
---
Finding 10 — High: readdocument() 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/docreadertool.py - line 259 — def readdocument(filepath: str, pages: str = "") -> str: - line 270 — path = Path(filepath) — followed only by path.exists() and path.isfile() checks before dispatching to format-specific readers - No call to safepath, safeuserpath, 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 filepath string, runs only path.exists() and path.isfile(), and dispatches to the appropriate reader. No call to safepath or safeuserpath exists in the function. A runtime probe confirmed:
- readdocument('/etc/passwd') returned HTTP 200 with 839 characters of content - readdocument('/etc/shadow') returned the full shadow password file - readdocument('/proc/self/environ') returned the full process environment, including OPENROUTERAPIKEY and TUSHARETOKEN in 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:
1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session. 2. 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"}' 3. Poll curl -s "http://HOST:8899/sessions/$SID/messages". Observe the agent invoke readdocument with filepath="/proc/self/environ" and return the full process environment in the message stream. 4. Observe OPENROUTERAPIKEY, TUSHARETOKEN, and any other variables in agent/.env appearing in plaintext. 5. Repeat with filepath="/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/idrsa, 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:
1. Different fix scope — F10's fix is a single missing call (safepath(filepath) in readdocument before line 270). F9's fix is in safeuserpath() 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 safeuserpath call to readdocument is insufficient because safeuserpath itself accepts /root and /app/agent/.env. Both surfaces need work.
2. Different reachability classes — F9 is reachable through tools that already gate on safeuserpath (ExtractShadowStrategyTool and several journal tools), so even a hypothetical F10 fix that switched readdocument to use safeuserpath would still leak /root/ because the envelope is broken. F9 is the structural defect; F10 is the missed call.
---
Suggested remediation
11. F9 — In safeuserpath() at pathutils.py:52-77, replace the Path.home() ∪ Path.cwd() envelope with a strict allowlist of operator-configured directories (e.g. an explicit BROKEREXPORTSDIR env var defaulting to /app/data/brokerexports/). 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).'
12. F10 — Add a safepath() (or safeuserpath()) call at docreadertool.py:270 before the existing path.exists() / path.isfile() checks. Once F9 is patched, the same allowlist will apply uniformly to both readdocument and the safeuserpath-gated tools.
13. 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.
---
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/vibe-trading-aito a version that resolves this vulnerability.Fixed in 0.1.7 - Configuration
Replace the Path.home() ∪ Path.cwd() envelope with a strict allowlist of operator-configured directories, such as BROKER_EXPORTS_DIR defaulting to /app/data/broker_exports/. Reject /root, /app/agent/.env, and /app/agent/uploads/.
agent/src/tools/path_utils.py:safe_user_path() allowed path envelope = BROKER_EXPORTS_DIR, defaulting to /app/data/broker_exports/ - Configuration
Call safe_path(file_path) before the path.exists() and path.is_file() checks so documents are opened only after sandbox validation.
agent/src/tools/doc_reader_tool.py:read_document() sandbox validation = safe_path(file_path) - Configuration
Add `RUN useradd -m vibe && chown -R vibe /app` and set `USER vibe` before `CMD` so the FastAPI process does not run as root.
Dockerfile container runtime user = vibe
Event History
Frequently Asked Questions
Who can exploit the exposed file-read functionality?
In the stated deployment, the file-read tools are auto-discovered by the LLM tool registry. When combined with GHSA-1/F1, any anonymous TCP client able to reach port 8899 can access them; the same file-access flaws also affect authenticated sessions.
What data is at risk in the shipped container configuration?
The FastAPI process runs as root because the runtime Dockerfile has no USER directive. The unrestricted read path can return any file readable by that process, including /etc/shadow, /etc/passwd, /proc/self/environ, and secret files mounted into the container; the path validation issue also permits files under /root and /app.
Do both findings need remediation?
Yes. The unrestricted read in read_document() requires adding the missing safe_path() sandbox call, while safe_user_path() requires tightening the allowed path envelope in path_utils.py. Fixing only one leaves a separate file-read exposure.