Skip to content

[Security] TinyAGI unauthenticated control-plane routes allow system prompt overwrite and daemon restart #294

Description

@YLChen-007

Advisory Details

Title: TinyAGI unauthenticated control-plane routes allow system prompt overwrite and daemon restart

Description:

TinyAGI exposes privileged control-plane HTTP routes without authentication or authorization checks. An unauthenticated client that can reach the API can overwrite an agent workspace AGENTS.md file through PUT /api/agents/:id/system-prompt and can force the main daemon to restart through POST /api/services/restart.

Summary

The API server mounts agent-administration and service-lifecycle routes after only a global CORS middleware, with no owner or operator gate in front of them. In practice, this lets an unauthenticated caller modify persistent agent prompt state and trigger process restart over the public REST API. I reproduced both effects end-to-end against TinyAGI v0.0.20 using the real daemon entrypoint and confirmed the file write and restart by independent observation.

Details

The issue is a missing authentication and authorization guard on control-plane routes.

At API startup, the server applies CORS globally and then mounts the route modules directly:

app.use('/*', cors());
app.route('/', agentsRoutes);
app.route('/', createServicesRoutes(services));

There is no authentication middleware or route-level permission check before privileged routes are exposed.

The first sink is the system-prompt write endpoint in packages/server/src/routes/agents.ts. It accepts attacker-controlled JSON and writes the supplied content directly into the target agent workspace file:

app.put('/api/agents/:id/system-prompt', async (c) => {
    const body = await c.req.json() as { content: string };
    const agentsMd = path.join(agent.working_directory, 'AGENTS.md');
    fs.writeFileSync(agentsMd, body.content || '', 'utf8');
    return c.json({ ok: true });
});

The second sink is the service restart endpoint in packages/server/src/routes/services.ts. It returns success and then asynchronously invokes the daemon restart callback with no caller validation:

app.post('/api/services/restart', (c) => {
    const response = c.json({ ok: true, action: 'restart' });
    setTimeout(() => handlers!.restart!(), 100);
    return response;
});

In the main daemon, that callback is wired to shutdown(75), so the impact is externally observable as a forced restart:

restart() {
    log('INFO', 'Restart requested via API');
    shutdown(75);
},

I verified the issue against the real daemon entrypoint packages/main/dist/index.js in an isolated TINYAGI_HOME. The exploit path was:

  1. Start TinyAGI with a temporary workspace containing a tinyagi agent.
  2. Call GET /api/status to confirm liveness.
  3. Call PUT /api/agents/tinyagi/system-prompt with a canary string.
  4. Read the workspace AGENTS.md file independently and confirm the canary was persisted.
  5. Call POST /api/services/restart.
  6. Observe the daemon exit with restart code 75.

This is not a static grep or mock-based result. The requests went through the public REST API, the AGENTS.md sink was verified on disk, and the restart effect was verified from the process exit code and daemon logs.

PoC

Prerequisites

  • A checkout of https://github.com/TinyAGI/tinyclaw at v0.0.20
  • Node.js installed
  • Python 3 installed
  • Project dependencies installed and the daemon built so packages/main/dist/index.js exists
  • No other process listening on TCP port 3777

Reproduction Steps

  1. Check out the affected release and build it:

    git clone https://github.com/TinyAGI/tinyclaw.git
    cd tinyclaw
    git checkout v0.0.20
    npm install
    npm run build
  2. Create a directory under the repository root for the PoC files:

    mkdir -p poc-ghsa-jr6x
  3. Download the helper and exploit scripts:

    curl -L https://gist.githubusercontent.com/YLChen-007/a28389aaaa6964914c3c7d3bc41cd139/raw/9ee4d8216f58a226a2cdb497fd29d09b72caa7c0/common.py -o poc-ghsa-jr6x/common.py
    curl -L https://gist.githubusercontent.com/YLChen-007/6edd2d28bdf320e142ad557b42e5ae13/raw/e7c6700d034f8c1055eb2d1dd690139da6e00bd3/verification_test.py -o poc-ghsa-jr6x/verification_test.py
    curl -L https://gist.githubusercontent.com/YLChen-007/e714ce86f34b4f4895756165272b3658/raw/5ca93ef6d46ff591d0ecb94394ca293e93083951/control-no-attack.py -o poc-ghsa-jr6x/control-no-attack.py
  4. Run the control script first to establish baseline behavior:

    python3 poc-ghsa-jr6x/control-no-attack.py
  5. Run the exploit script:

    python3 poc-ghsa-jr6x/verification_test.py
  6. Confirm that the control run reports [CONTROL-PASS] and that the exploit run reports both:

    [Exploit] PUT /api/agents/tinyagi/system-prompt -> 200, canary_written=True
    [Independent Observation] daemon exited with restart code 75: True
    

Log of Evidence

Control run:

[Mode] End-to-End
[Control] Same environment, defect-triggering input removed
[Liveness] GET /api/status -> 200, running=True
[Baseline] Canary absent before control run: True
[Independent Observation] Canary absent without attack input: True
[Independent Observation] daemon still running without restart call: True
[CONTROL-PASS] Baseline remained protected when trigger input was absent.

Exploit run:

[Mode] End-to-End
[Test Input] unauthenticated HTTP client
[Interface] public REST API
[Liveness] GET /api/status -> 200, running=True
[Baseline] Canary absent before attack: True
[Exploit] PUT /api/agents/tinyagi/system-prompt -> 200, canary_written=True
[Exploit] POST /api/services/restart -> 200, body={'ok': True, 'action': 'restart'}
[Independent Observation] daemon exited with restart code 75: True
[Daemon Output]
[2026-06-18T22:46:14.818Z] [INFO] Restart requested via API
[2026-06-18T22:46:14.818Z] [INFO] Restarting queue processor...
[DEFECT-CONFIRMED] Unauthenticated caller reached privileged control-plane actions.

Impact

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

  • Persistently replace an agent's system prompt by overwriting the workspace AGENTS.md
  • Force the TinyAGI daemon to restart on demand

The demonstrated impact is high-integrity compromise of agent control-plane state and high-availability impact on the daemon. In practice, rewriting the system prompt also creates a durable foothold over later agent behavior until the prompt file is corrected.

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:H

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-L63 The API server applies only global CORS and then mounts the privileged route modules directly, leaving the control plane exposed without authentication or authorization middleware.
https://github.com/TinyAGI/tinyclaw/blob/1ae8b4eba2fcb963a076655a7293fc77eafeb84c/packages/server/src/routes/agents.ts#L225-L235 PUT /api/agents/:id/system-prompt accepts attacker-controlled content and writes it directly to the target agent workspace AGENTS.md file.
https://github.com/TinyAGI/tinyclaw/blob/1ae8b4eba2fcb963a076655a7293fc77eafeb84c/packages/server/src/routes/services.ts#L67-L75 POST /api/services/restart returns success and schedules the restart handler with no caller validation, exposing privileged daemon lifecycle control to unauthenticated clients.
https://github.com/TinyAGI/tinyclaw/blob/1ae8b4eba2fcb963a076655a7293fc77eafeb84c/packages/main/src/index.ts#L232-L240 The server wires the restart callback to shutdown(75), making the unauthenticated POST /api/services/restart call terminate and restart the main TinyAGI process.

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