PT-2026-111186
Published
2026-10-02
·
Updated
2026-10-02
Description by
CVSS 3.1
7.5
HighMetricsAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
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 theHOSTplaceholder 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 DEMO,FAKE KEY,FAKE SECRET " > /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.
Found an issue in the description? Have something to add? Feel free to write us 👾