Advisory Details
Title: TinyAGI unauthenticated agent ID path traversal escapes the configured workspace root
Description:
Summary
TinyAGI exposes unauthenticated PUT /api/settings and POST /api/message endpoints that let a remote client define an agent with the id .. and then route a message to @... The queue processor later computes path.join(workspacePath, agentId) and bootstraps that agent directory. When agentId is .., TinyAGI initializes the agent in the parent of the configured workspace path instead of inside the intended per-agent workspace root.
Details
I verified this issue end to end against the highest current upstream release tag, v0.0.20, which resolves to commit 1ae8b4eba2fcb963a076655a7293fc77eafeb84c.
The vulnerable flow is:
packages/server/src/routes/settings.ts accepts attacker-controlled JSON and writes settings.agents keys to settings.json without validating the agent id.
packages/core/src/router.ts accepts @.. as a valid route target when the .. key exists in settings.
packages/core/src/invoke.ts uses the resolved agent id directly in path.join(workspacePath, agentId).
packages/core/src/agent.ts bootstraps the new agent directory at that derived path by creating .claude, AGENTS.md, .tinyagi/SOUL.md, memory, and synced skills.
The core issue is that a logical identifier is treated as a trusted filesystem path segment. There is no rejection of reserved ids such as . or .., no separator filtering, and no post-join containment check to ensure the final path remains inside workspacePath.
The vulnerable logic at v0.0.20 is straightforward:
const merged = { ...current, ...body } as Settings;
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(merged, null, 2) + '\n');
const candidateId = match[1].toLowerCase();
if (agents[candidateId]) {
return { agentId: candidateId, message };
}
const agentDir = path.join(workspacePath, agentId);
ensureAgentDirectory(agentDir);
With a benign id such as safeagent, bootstrap files are created under workspace/safeagent. With the attacker-controlled id .., the same bootstrap happens under TINYAGI_HOME, leaving the configured workspace empty and violating the intended workspace boundary.
PoC
Prerequisites
- Check out TinyAGI
v0.0.20 or another affected revision at or below that release.
- Install dependencies and build the project so
packages/main/dist/index.js exists.
- Have
python3 and node available.
- No authentication or special runtime flags are required.
Reproduction Steps
- Download the exploit script from: verification_test.py
- Download the control script from: control-safe-agent.py
- From the TinyAGI checkout, run the control case:
TINYAGI_ROOT=/path/to/tinyclaw python3 control-safe-agent.py
- Confirm the control case prints
[CONTROL-PROTECTED] and records bootstrap files only under workspace/safeagent.
- Run the exploit case:
TINYAGI_ROOT=/path/to/tinyclaw python3 verification_test.py
- Confirm the exploit case prints
[DEFECT-CONFIRMED].
- Inspect the generated
verification-result.json. In the vulnerable run, the temporary TINYAGI_HOME root contains .agents, .claude, and memory, while workspace_tree is empty and workspace_agent_exists is false.
Log of Evidence
Control run:
[CONTROL-PROTECTED]
"workspace_safe_exists": true
"root_agents_exists": false
"root_claude_exists": false
"root_memory_exists": false
Exploit run:
[INFO] [API] Settings updated
[INFO] [API] Message enqueued: @.. hello
[INFO] Processing [api] from API: @.. hello
[ERROR] Claude error (agent: ..): Custom provider 'missing' not found in settings.custom_providers
[DEFECT-CONFIRMED]
"root_agents_exists": true
"root_claude_exists": true
"root_memory_exists": true
"workspace_tree": []
"workspace_agent_exists": false
The provider lookup failure happens after invokeAgent() has already initialized the escaped directory, so the defect does not depend on a successful model invocation.
Impact
Any client that can reach TinyAGI's unauthenticated HTTP API can break the configured per-agent workspace boundary and force TinyAGI to create bootstrap files outside the intended workspace root. In practice this gives a remote attacker an unauthorized filesystem write primitive within TINYAGI_HOME, pollutes shared agent state, and breaks the isolation assumptions between agent workspaces and global application state.
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:N/I:H/A:L
Weaknesses
- CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Occurrences
Advisory Details
Title: TinyAGI unauthenticated agent ID path traversal escapes the configured workspace root
Description:
Summary
TinyAGI exposes unauthenticated
PUT /api/settingsandPOST /api/messageendpoints that let a remote client define an agent with the id..and then route a message to@... The queue processor later computespath.join(workspacePath, agentId)and bootstraps that agent directory. WhenagentIdis.., TinyAGI initializes the agent in the parent of the configured workspace path instead of inside the intended per-agent workspace root.Details
I verified this issue end to end against the highest current upstream release tag,
v0.0.20, which resolves to commit1ae8b4eba2fcb963a076655a7293fc77eafeb84c.The vulnerable flow is:
packages/server/src/routes/settings.tsaccepts attacker-controlled JSON and writessettings.agentskeys tosettings.jsonwithout validating the agent id.packages/core/src/router.tsaccepts@..as a valid route target when the..key exists in settings.packages/core/src/invoke.tsuses the resolved agent id directly inpath.join(workspacePath, agentId).packages/core/src/agent.tsbootstraps the new agent directory at that derived path by creating.claude,AGENTS.md,.tinyagi/SOUL.md,memory, and synced skills.The core issue is that a logical identifier is treated as a trusted filesystem path segment. There is no rejection of reserved ids such as
.or.., no separator filtering, and no post-join containment check to ensure the final path remains insideworkspacePath.The vulnerable logic at
v0.0.20is straightforward:With a benign id such as
safeagent, bootstrap files are created underworkspace/safeagent. With the attacker-controlled id.., the same bootstrap happens underTINYAGI_HOME, leaving the configured workspace empty and violating the intended workspace boundary.PoC
Prerequisites
v0.0.20or another affected revision at or below that release.packages/main/dist/index.jsexists.python3andnodeavailable.Reproduction Steps
TINYAGI_ROOT=/path/to/tinyclaw python3 control-safe-agent.py[CONTROL-PROTECTED]and records bootstrap files only underworkspace/safeagent.TINYAGI_ROOT=/path/to/tinyclaw python3 verification_test.py[DEFECT-CONFIRMED].verification-result.json. In the vulnerable run, the temporaryTINYAGI_HOMEroot contains.agents,.claude, andmemory, whileworkspace_treeis empty andworkspace_agent_existsisfalse.Log of Evidence
The provider lookup failure happens after
invokeAgent()has already initialized the escaped directory, so the defect does not depend on a successful model invocation.Impact
Any client that can reach TinyAGI's unauthenticated HTTP API can break the configured per-agent workspace boundary and force TinyAGI to create bootstrap files outside the intended workspace root. In practice this gives a remote attacker an unauthorized filesystem write primitive within
TINYAGI_HOME, pollutes shared agent state, and breaks the isolation assumptions between agent workspaces and global application state.Affected products
Severity
Weaknesses
Occurrences
PUT /api/settingsmerges attacker-controlled JSON into the persisted settings without validating agent ids before writingsettings.json.parseAgentRouting()accepts@..as a routable target when a..entry exists in the agents map.invokeAgent()joins the untrustedagentIddirectly intoworkspacePathand immediately passes the result to the filesystem bootstrap sink.ensureAgentDirectory()creates the resolved directory path recursively without verifying that the path remains under the configured workspace root.AGENTS.md,.tinyagi/SOUL.md, andmemoryinto that escaped location, making the workspace escape observable on disk.