Skip to content

[Security] TinyAGI unauthenticated administrative API allows persistent settings and agent prompt modification #292

Description

@YLChen-007

Advisory Details

Title: TinyAGI unauthenticated administrative API allows persistent settings and agent prompt modification

Description:
TinyAGI exposes state-changing control-plane API endpoints without any authentication or authorization checks. An unauthenticated client that can reach the API port can overwrite the global settings file, create or modify agent definitions, and write attacker-controlled prompt content into agent workspaces.

Summary

TinyAGI publishes PUT /api/settings and PUT /api/agents/:id on its Hono API with no auth barrier. Any unauthenticated HTTP client can use these routes to overwrite ${TINYAGI_HOME}/settings.json, add arbitrary agents entries, and write attacker-controlled content to <workspace>/<agent>/AGENTS.md. Because the daemon later consumes these files as control-plane state, this is a persistent compromise of TinyAGI runtime behavior rather than a transient API bug.

Details

The root cause is the absence of any authentication middleware or route-level authorization around administrative API modules. In packages/server/src/index.ts, the server enables global CORS and mounts the administrative route sets directly:

app.use('/*', cors());
app.route('/', agentsRoutes);
app.route('/', teamsRoutes);
app.route('/', settingsRoutes);

No owner/admin check is applied before the state-changing handlers run.

The first sink is packages/server/src/routes/settings.ts. PUT /api/settings accepts arbitrary JSON from the request body, merges it with the current settings object, and writes the result straight back to SETTINGS_FILE:

app.put('/api/settings', async (c) => {
    const body = await c.req.json();
    const current = getSettings();
    const merged = { ...current, ...body } as Settings;
    fs.writeFileSync(SETTINGS_FILE, JSON.stringify(merged, null, 2) + '\n');
});

The second sink is packages/server/src/routes/agents.ts. PUT /api/agents/:id accepts untrusted JSON, persists a new or modified agent entry via mutateSettings(...), and, if system_prompt is supplied, writes that content verbatim into the target workspace AGENTS.md file at packages/server/src/routes/agents.ts#L76-L77:

const settings = mutateSettings(s => {
    if (!s.agents) s.agents = {};
    s.agents[agentId] = {
        name: body.name!,
        provider: body.provider!,
        model: body.model!,
        working_directory: workingDir,
    };
});

if (body.system_prompt != null) {
    fs.writeFileSync(path.join(workingDir, 'AGENTS.md'), body.system_prompt, 'utf8');
}

I verified the issue end-to-end against the upstream v0.0.20 release commit 1ae8b4eba2fcb963a076655a7293fc77eafeb84c. The test started the real daemon with node packages/main/dist/index.js, sent unauthenticated requests to PUT /api/agents/attacker and PUT /api/settings, and independently confirmed the attacker-controlled values on disk in runtime-vuln/settings.json and runtime-vuln/workspace/attacker/AGENTS.md.

PoC

Prerequisites

  • Upstream TinyAGI checkout at v0.0.20 or another affected release <= 0.0.20
  • Node.js and repository dependencies installed
  • Built server artifacts from npm run build
  • Python 3 available to run the PoC harness
  • Ability to run the TinyAGI daemon locally on port 3777

Reproduction Steps

  1. Check out an affected release and build it:

    git checkout v0.0.20
    npm install
    npm run build
  2. Download the verification PoC from: verification_test.py

  3. From the TinyAGI repository root, run:

    python3 ./verification_test.py

    If you save the script outside the repository root, set the repo path explicitly:

    TINYAGI_REPO_ROOT=/path/to/tinyclaw python3 /path/to/verification_test.py
  4. The script launches the real daemon with an isolated TINYAGI_HOME, sends these unauthenticated administrative requests, and then checks the persisted sink files:

    PUT /api/agents/attacker
    {"name":"Attacker Agent","provider":"anthropic","model":"sonnet","system_prompt":"owned-by-unauth-client"}
    
    PUT /api/settings
    {"monitoring":{"heartbeat_interval":1337}}
  5. Observe the resulting disk state under the isolated runtime:

    • runtime-vuln/settings.json contains a new agents.attacker entry
    • runtime-vuln/settings.json contains monitoring.heartbeat_interval = 1337
    • runtime-vuln/workspace/attacker/AGENTS.md contains owned-by-unauth-client
  6. For a baseline comparison, download and run the control script from: control-readonly-baseline.py

    python3 ./control-readonly-baseline.py

    That script uses the same real service, but only performs GET /api/agents and GET /api/settings, and confirms that read-only traffic does not create workspace/attacker/AGENTS.md or mutate settings.json.

Log of Evidence

Verification run:

[Mode] End-to-End
[Test Input] unauthenticated HTTP client
[Interface] PUT /api/agents/:id and PUT /api/settings
[Expectation] administrative state changes succeed without authentication
PUT /api/agents/attacker -> HTTP 200, prompt_exists=True, content_match=True
PUT /api/settings -> HTTP 200, on_disk.monitoring.heartbeat_interval=1337
[DEFECT-CONFIRMED] Unauthenticated callers can mutate persistent admin state via the public API.

Control run:

[Mode] End-to-End
[Control] Same public HTTP interface without state-changing input
[Interface] GET /api/agents and GET /api/settings
[Expectation] baseline reads succeed and no new workspace/config mutation occurs
GET /api/agents -> HTTP 200, attacker_present=False
GET /api/settings -> HTTP 200, on_disk.monitoring.heartbeat_interval=None
workspace/attacker/AGENTS.md exists=False
[CONTROL-PASS] Baseline interface access does not create agent files or mutate settings.

Impact

This is an unauthenticated administrative control-plane compromise. Any client that can reach the TinyAGI API can:

  • overwrite persistent global settings in settings.json
  • create or modify agent definitions without operator approval
  • write attacker-controlled prompt content into workspace AGENTS.md
  • influence later daemon and agent behavior using persisted malicious state

In practice, this breaks the integrity of the TinyAGI deployment and can be used to poison agent behavior, alter operational settings, and disrupt or redirect future automated actions. Deployments that expose the API beyond a single trusted local operator are especially at risk.

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:L/I:H/A:L

Weaknesses

  • CWE: CWE-306: Missing Authentication for Critical Function

Occurrences

Permalink Description
https://github.com/TinyAGI/tinyclaw/blob/1ae8b4eba2fcb963a076655a7293fc77eafeb84c/packages/server/src/index.ts#L46-L53 The API server enables CORS and mounts administrative route modules directly, with no authentication middleware or authorization guard in front of agentsRoutes and settingsRoutes.
https://github.com/TinyAGI/tinyclaw/blob/1ae8b4eba2fcb963a076655a7293fc77eafeb84c/packages/server/src/routes/settings.ts#L35-L42 PUT /api/settings accepts attacker-controlled JSON, merges it into the live settings object, and writes the result directly to SETTINGS_FILE.
https://github.com/TinyAGI/tinyclaw/blob/1ae8b4eba2fcb963a076655a7293fc77eafeb84c/packages/server/src/routes/agents.ts#L42-L65 PUT /api/agents/:id accepts untrusted JSON and persists a new or modified agent entry to settings.json without checking whether the caller is authorized to perform administrative changes.
https://github.com/TinyAGI/tinyclaw/blob/1ae8b4eba2fcb963a076655a7293fc77eafeb84c/packages/server/src/routes/agents.ts#L76-L77 If the request includes system_prompt, the handler writes attacker-controlled content verbatim into the target workspace AGENTS.md file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions