Advisory Details
Title: Unauthenticated prompt_file update allows arbitrary local file read into provider-bound prompts
Description:
TinyAGI <= 0.0.20 allows any client that can reach the HTTP management API to set an agent's prompt_file to an arbitrary readable local path and have that file's contents appended to the next provider-bound system prompt.
Summary
The PUT /api/agents/:id management route accepts attacker-controlled prompt_file values without authentication or path validation. On the next normal POST /api/message invocation, TinyAGI reads the referenced host file with fs.readFileSync() and appends the contents to the system prompt before sending it to the configured model provider path. I reproduced this end-to-end on the latest release tag v0.0.20 by setting prompt_file=/etc/hosts and observing /etc/hosts content inside the captured provider-bound prompt.
Details
At release v0.0.20, the API server mounts agentsRoutes immediately after a global CORS middleware and does not place an authentication guard in front of the agent-management routes. The PUT /api/agents/:id handler then persists body.prompt_file directly into agent settings:
s.agents[agentId] = {
name: body.name!,
provider: body.provider!,
model: body.model!,
working_directory: workingDir,
...(body.prompt_file ? { prompt_file: body.prompt_file } : {}),
};
That value later reaches the normal invocation path. invokeAgent() passes agent.prompt_file into buildSystemPrompt(), and buildSystemPrompt() reads the referenced file from the host filesystem and appends the result to the system prompt:
if (configPromptFile) {
promptFileContent = fs.readFileSync(configPromptFile, 'utf8').trim();
if (promptFileContent) {
prompt += '\n\n' + promptFileContent;
}
}
In the verified path, packages/core/src/adapters/opencode.ts serializes the resulting prompt into OPENCODE_CONFIG_CONTENT, which is then consumed by the provider-side command. The vulnerability therefore crosses the trust boundary from untrusted HTTP input into local filesystem reads and then into provider-facing prompt material.
I verified the issue on the v0.0.20 code path with the real TinyAGI daemon started from packages/main/dist/index.js. The experiment run set prompt_file=/etc/hosts, issued a normal POST /api/message, and captured /etc/hosts content inside captured_prompt.json. The control run set prompt_file to a workspace-local allowed-prompt.md file and confirmed that the workspace canary appeared while /etc/hosts content did not. The reproduction uses a local fake opencode binary only as a capture sink; the unsafe file read itself happens earlier in buildSystemPrompt().
PoC
Prerequisites
- A checkout of the canonical upstream repository
https://github.com/TinyAGI/tinyagi at or before v0.0.20.
- Node.js dependencies installed and build artifacts present so
packages/main/dist/index.js exists. Example: npm install && npm run build.
- Python 3 available locally.
- Ability to bind the local TinyAGI API port
3777.
- A readable benign target file for verification. The provided PoC uses
/etc/hosts.
Reproduction Steps
- Download the verification PoC from: verification_test.py
- Download the shared harness from: harness.py
- Download the local capture helper from: opencode, save it as
fakebin/opencode, and run chmod +x fakebin/opencode
- Download the control script from: control-workspace-prompt.py
- From the repository root, place the downloaded files in a directory together, or reuse the copies already present under
llm-enhance/cve-finding/similar/info-leak/Advisory-GHSA-jq4x-98m3-ggq6-prompt-file-local-read-exp/
- Run the verification flow:
python3 llm-enhance/cve-finding/similar/info-leak/Advisory-GHSA-jq4x-98m3-ggq6-prompt-file-local-read-exp/verification_test.py
- Run the control flow:
python3 llm-enhance/cve-finding/similar/info-leak/Advisory-GHSA-jq4x-98m3-ggq6-prompt-file-local-read-exp/control-workspace-prompt.py
- Inspect
captured_prompt.json, verification.log, control.log, and server.stdout.log
Log of Evidence
[Mode] End-to-End
[Test Input] unauthenticated HTTP client controls agent.prompt_file
[Interface] PUT /api/agents/:id then POST /api/message
[Processing] settings mutation -> invokeAgent() -> buildSystemPrompt()
[Sink] fs.readFileSync(configPromptFile) content forwarded to provider adapter
Prompt file under test: /etc/hosts
Leaked /etc/hosts content into provider-bound prompt: True
[DEFECT CONFIRMED] Arbitrary host file contents reached the provider-facing prompt over the public API flow
[Mode] End-to-End
[Control] Same public API flow with a workspace-local prompt file
[Interface] PUT /api/agents/:id then POST /api/message
Workspace canary present in provider-bound prompt: True
/etc/hosts content present in control prompt: False
[CONTROL PASSED] Legitimate workspace prompt content is forwarded while unrelated host file content is absent
server.stdout.log from the reproduction also showed the real request and invocation path:
[INFO] API server listening on http://localhost:3777
[INFO] [API] Agent 'reader' saved
[INFO] [API] Message enqueued: reply with one word
[INFO] Processing [api] from API: reply with one word
[DEBUG] Using OpenCode CLI (agent: reader, model: dummy)
Impact
This is an arbitrary local file read that can be triggered by any client that can reach the TinyAGI HTTP API. An attacker can coerce TinyAGI into reading host files such as credentials, API keys, cloud metadata, SSH material, configuration files, or other local secrets that are readable by the TinyAGI service account, and then leak those contents into provider-bound prompt traffic or downstream logs. I verified the downstream leak through the opencode adapter; because the unsafe read happens before adapter invocation, the confidentiality risk applies to the prompt-construction path itself, and any deployment exposing the management API to untrusted clients is affected.
Affected products
- Ecosystem: npm
- Package name: tinyagi
- Affected versions: <= 0.0.20
- Patched versions:
Severity
- Severity: High
- Vector string: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Weaknesses
- CWE: CWE-73: External Control of File Name or Path
Occurrences
| Permalink |
Description |
|
// CORS middleware |
|
app.use('/*', cors()); |
|
|
|
// Mount route modules |
|
app.route('/', messagesRoutes); |
|
app.route('/', agentsRoutes); |
|
app.route('/', teamsRoutes); |
|
The API server applies only CORS and mounts agentsRoutes directly, leaving the agent-management endpoints reachable without an authentication gate. |
|
const settings = mutateSettings(s => { |
|
if (!s.agents) s.agents = {}; |
|
s.agents[agentId] = { |
|
name: body.name!, |
|
provider: body.provider!, |
|
model: body.model!, |
|
working_directory: workingDir, |
|
...(body.prompt_file ? { prompt_file: body.prompt_file } : {}), |
|
}; |
|
PUT /api/agents/:id persists attacker-controlled prompt_file input into agent settings without validation or path restriction. |
|
// Build system prompt in-memory (built-in instructions + teammates + memory + user customization) |
|
const systemPrompt = buildSystemPrompt(agentId, agentDir, agents, teams, agent.system_prompt, agent.prompt_file); |
|
The normal message execution path forwards agent.prompt_file into buildSystemPrompt() during a standard agent invocation. |
|
// Append config system prompt (from settings.json) |
|
let promptFileContent = ''; |
|
if (configPromptFile) { |
|
try { |
|
promptFileContent = fs.readFileSync(configPromptFile, 'utf8').trim(); |
|
if (promptFileContent) { |
|
prompt += '\n\n' + promptFileContent; |
|
} |
|
buildSystemPrompt() directly reads configPromptFile from the local filesystem with fs.readFileSync() and appends the contents to the system prompt. |
|
// Pass system prompt via OPENCODE_CONFIG_CONTENT env var |
|
if (systemPrompt) { |
|
const configContent = JSON.stringify({ |
|
agent: { |
|
[agentId]: { |
|
prompt: systemPrompt |
|
} |
|
} |
|
}); |
|
envOverrides.OPENCODE_CONFIG_CONTENT = configContent; |
|
The verified provider path serializes the resulting system prompt into OPENCODE_CONFIG_CONTENT, carrying the leaked file contents into downstream provider-facing input. |
Advisory Details
Title: Unauthenticated
prompt_fileupdate allows arbitrary local file read into provider-bound promptsDescription:
TinyAGI
<= 0.0.20allows any client that can reach the HTTP management API to set an agent'sprompt_fileto an arbitrary readable local path and have that file's contents appended to the next provider-bound system prompt.Summary
The
PUT /api/agents/:idmanagement route accepts attacker-controlledprompt_filevalues without authentication or path validation. On the next normalPOST /api/messageinvocation, TinyAGI reads the referenced host file withfs.readFileSync()and appends the contents to the system prompt before sending it to the configured model provider path. I reproduced this end-to-end on the latest release tagv0.0.20by settingprompt_file=/etc/hostsand observing/etc/hostscontent inside the captured provider-bound prompt.Details
At release
v0.0.20, the API server mountsagentsRoutesimmediately after a global CORS middleware and does not place an authentication guard in front of the agent-management routes. ThePUT /api/agents/:idhandler then persistsbody.prompt_filedirectly into agent settings:That value later reaches the normal invocation path.
invokeAgent()passesagent.prompt_fileintobuildSystemPrompt(), andbuildSystemPrompt()reads the referenced file from the host filesystem and appends the result to the system prompt:In the verified path,
packages/core/src/adapters/opencode.tsserializes the resulting prompt intoOPENCODE_CONFIG_CONTENT, which is then consumed by the provider-side command. The vulnerability therefore crosses the trust boundary from untrusted HTTP input into local filesystem reads and then into provider-facing prompt material.I verified the issue on the
v0.0.20code path with the real TinyAGI daemon started frompackages/main/dist/index.js. The experiment run setprompt_file=/etc/hosts, issued a normalPOST /api/message, and captured/etc/hostscontent insidecaptured_prompt.json. The control run setprompt_fileto a workspace-localallowed-prompt.mdfile and confirmed that the workspace canary appeared while/etc/hostscontent did not. The reproduction uses a local fakeopencodebinary only as a capture sink; the unsafe file read itself happens earlier inbuildSystemPrompt().PoC
Prerequisites
https://github.com/TinyAGI/tinyagiat or beforev0.0.20.packages/main/dist/index.jsexists. Example:npm install && npm run build.3777./etc/hosts.Reproduction Steps
fakebin/opencode, and runchmod +x fakebin/opencodellm-enhance/cve-finding/similar/info-leak/Advisory-GHSA-jq4x-98m3-ggq6-prompt-file-local-read-exp/python3 llm-enhance/cve-finding/similar/info-leak/Advisory-GHSA-jq4x-98m3-ggq6-prompt-file-local-read-exp/verification_test.pypython3 llm-enhance/cve-finding/similar/info-leak/Advisory-GHSA-jq4x-98m3-ggq6-prompt-file-local-read-exp/control-workspace-prompt.pycaptured_prompt.json,verification.log,control.log, andserver.stdout.logLog of Evidence
server.stdout.logfrom the reproduction also showed the real request and invocation path:Impact
This is an arbitrary local file read that can be triggered by any client that can reach the TinyAGI HTTP API. An attacker can coerce TinyAGI into reading host files such as credentials, API keys, cloud metadata, SSH material, configuration files, or other local secrets that are readable by the TinyAGI service account, and then leak those contents into provider-bound prompt traffic or downstream logs. I verified the downstream leak through the
opencodeadapter; because the unsafe read happens before adapter invocation, the confidentiality risk applies to the prompt-construction path itself, and any deployment exposing the management API to untrusted clients is affected.Affected products
Severity
Weaknesses
Occurrences
tinyagi/packages/server/src/index.ts
Lines 46 to 52 in 1ae8b4e
agentsRoutesdirectly, leaving the agent-management endpoints reachable without an authentication gate.tinyagi/packages/server/src/routes/agents.ts
Lines 56 to 64 in 1ae8b4e
PUT /api/agents/:idpersists attacker-controlledprompt_fileinput into agent settings without validation or path restriction.tinyagi/packages/core/src/invoke.ts
Lines 201 to 202 in 1ae8b4e
agent.prompt_fileintobuildSystemPrompt()during a standard agent invocation.tinyagi/packages/core/src/agent.ts
Lines 208 to 215 in 1ae8b4e
buildSystemPrompt()directly readsconfigPromptFilefrom the local filesystem withfs.readFileSync()and appends the contents to the system prompt.tinyagi/packages/core/src/adapters/opencode.ts
Lines 27 to 36 in 1ae8b4e
OPENCODE_CONFIG_CONTENT, carrying the leaked file contents into downstream provider-facing input.