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
- Download the verification PoC from: verification_min.py
- Download the matched control PoC from: control_min.py
- From the repository root, ensure the built runtime exists. If
packages/main/dist/index.js is missing, run npm run build.
- 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
- 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.
- Observe that the verification script records
[DEFECT-CONFIRMED] and that the tmux capture contains FORGED_APPROVAL_PROMPT [permission requested] Allow dangerous tool? (y/N).
- 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
- 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. |
Advisory Details
Title: TinyAGI Terminal Escape Injection via
POST /api/messageAllows Operator Log SpoofingDescription:
Summary
TinyAGI logs attacker-controlled
messagecontent from the unauthenticatedPOST /api/messageentrypoint directly to the operator terminal in affected releases. Because the logging path does not neutralize ANSI/CSI control sequences before callingconsole.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 fromv0.0.15throughv0.0.20.The dataflow is:
POST /api/message->packages/server/src/routes/messages.ts->enqueueMessage()-> queue consumer inpackages/main/src/index.ts->packages/core/src/logging.ts->console.log()-> terminal interprets attacker-supplied control bytesThe 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:
The same untrusted value is later read back from the queue and written into a second terminal-facing log line:
The shared sink does not sanitize terminal control sequences before printing:
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 asTinyAGI/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 tagv0.0.20.PoC
Prerequisites
packages/main/dist/index.jsexiststmuxandncinstalledPOST /api/messageReproduction Steps
packages/main/dist/index.jsis missing, runnpm run build.python3 llm-enhance/cve-finding/similar/rce/Advisory-GHSA-4hmj-39m8-jwc7-terminal-log-spoofing-exp/verification_test.pyGET /api/statusto return200, posts a benign baseline message, then posts the forged payload containing ANSI/CSI terminal control bytes.[DEFECT-CONFIRMED]and that the tmux capture containsFORGED_APPROVAL_PROMPT [permission requested] Allow dangerous tool? (y/N).python3 llm-enhance/cve-finding/similar/rce/Advisory-GHSA-4hmj-39m8-jwc7-terminal-log-spoofing-exp/control-normal-behavior.py[CONTROL-PASS]and only showsCONTROL_SAFE_PAYLOAD, with no forged approval prompt text.Log of Evidence
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
Severity
Weaknesses
Occurrences
tinyagi/packages/server/src/routes/messages.ts
Line 60 in 1ae8b4e
POST /api/messagewrites attacker-controlledmessagecontent directly into a terminal-facing log entry without neutralizing terminal control sequences first.tinyagi/packages/main/src/index.ts
Lines 54 to 57 in 1ae8b4e
rawMessageand logs it again, preserving source-to-sink continuity for the escape-sequence payload.tinyagi/packages/core/src/logging.ts
Lines 4 to 8 in 1ae8b4e
log()sink forwards the composed message toconsole.log()with no ANSI/CSI stripping, allowing the terminal to interpret attacker-controlled control bytes.