Skip to content

[Security] TinyAGI Terminal Escape Injection via POST /api/message Allows Operator Log Spoofing #290

Description

@YLChen-007

Advisory Details

Title: TinyAGI Terminal Escape Injection via POST /api/message Allows Operator Log Spoofing

Description:

Summary

TinyAGI logs attacker-controlled message content from the unauthenticated POST /api/message entrypoint directly to the operator terminal in affected releases. Because the logging path does not neutralize ANSI/CSI control sequences before calling console.log(), a remote client can visibly rewrite prior terminal output and spoof approval or status prompts in the live daemon console.

Details

I verified the issue end to end against the latest published release tag, v0.0.20, and then checked earlier tags to bound the affected range. The vulnerable logging path is present from v0.0.15 through v0.0.20.

The dataflow is:

POST /api/message -> packages/server/src/routes/messages.ts -> enqueueMessage() -> queue consumer in packages/main/src/index.ts -> packages/core/src/logging.ts -> console.log() -> terminal interprets attacker-supplied control bytes

The first unsafe log happens at the HTTP ingestion point. The API accepts a caller-controlled string and writes it directly into a terminal-facing log line:

log('INFO', `[API] Message enqueued: ${message}`);

The same untrusted value is later read back from the queue and written into a second terminal-facing log line:

log('INFO', `Processing [${isInternal ? 'internal' : channel}] ${isInternal ? `@${data.fromAgent}→@${preRoutedAgent}` : `from ${sender}`}: ${rawMessage}`);

The shared sink does not sanitize terminal control sequences before printing:

export function log(level: string, message: string): void {
    const timestamp = new Date().toISOString();
    const logMessage = `[${timestamp}] [${level}] ${message}\n`;
    console.log(logMessage.trim());
    fs.appendFileSync(LOG_FILE, logMessage);
}

With a payload such as \u001b[2K\u001b[1A\u001b[2KFORGED_APPROVAL_PROMPT [permission requested] Allow dangerous tool? (y/N), the terminal clears and rewrites visible lines instead of rendering the bytes literally. In my verification run, the live tmux pane showed a forged approval-style prompt while the matched control, which removed only the ANSI/CSI trigger, did not.

One repo detail is worth calling out for maintainers: the local checkout still uses the legacy remote name TinyAGI/tinyclaw, but GitHub now resolves the canonical upstream repository as TinyAGI/tinyagi ("TinyAGI is the agent teams orchestrator for One Person Company. (fka TinyClaw)"). The occurrence permalinks below therefore use the canonical upstream repository and the full 40-character commit behind release tag v0.0.20.

PoC

Prerequisites

  • TinyAGI checked out from the canonical upstream repository and built so packages/main/dist/index.js exists
  • Node.js installed
  • Python 3 installed
  • tmux and nc installed
  • No API authentication required for POST /api/message
  • Ability to run the TinyAGI daemon locally and observe terminal output

Reproduction Steps

  1. Download the verification PoC from: verification_min.py
  2. Download the matched control PoC from: control_min.py
  3. From the repository root, ensure the built runtime exists. If packages/main/dist/index.js is missing, run npm run build.
  4. Run the end-to-end verification script:
    python3 llm-enhance/cve-finding/similar/rce/Advisory-GHSA-4hmj-39m8-jwc7-terminal-log-spoofing-exp/verification_test.py
  5. The script starts the real TinyAGI daemon inside a temporary tmux session, waits for GET /api/status to return 200, posts a benign baseline message, then posts the forged payload containing ANSI/CSI terminal control bytes.
  6. Observe that the verification script records [DEFECT-CONFIRMED] and that the tmux capture contains FORGED_APPROVAL_PROMPT [permission requested] Allow dangerous tool? (y/N).
  7. Run the matched control script:
    python3 llm-enhance/cve-finding/similar/rce/Advisory-GHSA-4hmj-39m8-jwc7-terminal-log-spoofing-exp/control-normal-behavior.py
  8. Observe that the control records [CONTROL-PASS] and only shows CONTROL_SAFE_PAYLOAD, with no forged approval prompt text.

Log of Evidence

Verification mode: End-to-End
Reachability: HTTP POST /api/message -> enqueueMessage/log() -> processMessage/log() -> terminal PTY
Liveness GET /api/status: 200 {"ok":true,"uptime":2,"server":{"running":true,"port":47917},"channels":{},"heartbeat":{"running":true,"interval":3600,"lastSent":{}}}
Baseline HTTP status: 200, body: {"ok":true,"messageId":"api_cw172zle"}
Forged HTTP status: 200, body: {"ok":true,"messageId":"api_yz6w2bar"}
FORGED_APPROVAL_PROMPT [permission requested] Allow dangerous tool? (y/N)
Result: [DEFECT-CONFIRMED]
Verification mode: End-to-End control
Control input removes only the ANSI escape sequence trigger
Liveness GET /api/status: 200 {"ok":true,"uptime":2,"server":{"running":true,"port":47918},"channels":{},"heartbeat":{"running":true,"interval":3600,"lastSent":{}}}
Control HTTP status: 200, body: {"ok":true,"messageId":"api_58xqakpj"}
CONTROL_SAFE_PAYLOAD
Result: [CONTROL-PASS]

Impact

This is a terminal escape injection and log spoofing vulnerability. Any client that can reach TinyAGI's message API can inject control sequences that alter what an operator sees in the live daemon console. That breaks the integrity of operator-visible logs and can be used to spoof approval prompts, overwrite status lines, or hide nearby output in a monitoring session.

I did not need local shell access, database tampering, mocked calls, or source changes to reproduce this. The direct, demonstrated impact is operator deception and loss of trustworthy terminal logs. In environments where operators rely on the console to decide whether to approve, investigate, or trust an action, the practical risk increases accordingly.

Affected products

  • Ecosystem: npm
  • Package name: tinyagi
  • Affected versions: >= 0.0.15, <= 0.0.20
  • Patched versions:

Severity

  • Severity: Medium
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N

Weaknesses

  • CWE: CWE-150: Improper Neutralization of Escape, Meta, or Control Sequences

Occurrences

Permalink Description
log('INFO', `[API] Message enqueued: ${message}`);
POST /api/message writes attacker-controlled message content directly into a terminal-facing log entry without neutralizing terminal control sequences first.
const { channel, sender, message: rawMessage, messageId, agent: preRoutedAgent } = data;
const isInternal = !!data.fromAgent;
log('INFO', `Processing [${isInternal ? 'internal' : channel}] ${isInternal ? `@${data.fromAgent}→@${preRoutedAgent}` : `from ${sender}`}: ${rawMessage}`);
The queue processor reloads the same persisted message as rawMessage and logs it again, preserving source-to-sink continuity for the escape-sequence payload.
export function log(level: string, message: string): void {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
console.log(logMessage.trim());
fs.appendFileSync(LOG_FILE, logMessage);
The shared log() sink forwards the composed message to console.log() with no ANSI/CSI stripping, allowing the terminal to interpret attacker-controlled control bytes.

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