Skip to content

[Security] TinyAGI Anthropic Adapter Disables Claude Dangerous-Tool Confirmation for Unauthenticated POST /api/message Requests #291

Description

@YLChen-007

Advisory Details

Title: TinyAGI Anthropic Adapter Disables Claude Dangerous-Tool Confirmation for Unauthenticated POST /api/message Requests

Description:

TinyAGI launches the Anthropic claude CLI with --dangerously-skip-permissions unconditionally. As a result, any remote client that can reach POST /api/message can drive an Anthropic-backed agent session in which Claude's dangerous-tool confirmation barrier has already been disabled before the attacker-controlled prompt is processed.

Summary

In affected TinyAGI releases, unauthenticated HTTP input sent to POST /api/message is queued and routed into the normal Anthropic agent path. That path always prepends --dangerously-skip-permissions before invoking claude -p <message>. This removes the provider-side confirmation step that should otherwise protect dangerous tool actions, so remote clients can cause request-driven Claude sessions to start with the approval barrier already bypassed.

Details

I verified the issue against the latest published release, v0.0.20, and then checked earlier tags to bound the affected range. The unconditional --dangerously-skip-permissions flag is present from v0.0.16 through v0.0.20. The full 40-character commit behind the highest affected release tag is:

1ae8b4eba2fcb963a076655a7293fc77eafeb84c

This checkout still uses the legacy remote name TinyAGI/tinyclaw, but GitHub's canonical upstream repository now resolves to TinyAGI/tinyagi. The occurrence permalinks below therefore use the canonical repository and the release commit above.

The relevant source-to-sink path is:

POST /api/message -> enqueueMessage() -> queue consumer processMessage() -> invokeAgent() -> Anthropic adapter -> claude --dangerously-skip-permissions ... -p <message>

At the sink, the Anthropic adapter starts every Claude invocation with the dangerous approval bypass already enabled:

const args = ['--dangerously-skip-permissions'];
if (model) args.push('--model', model);
if (systemPrompt) args.push('--system-prompt', systemPrompt);
if (continueConversation) args.push('-c');

The remote input path is standard application behavior rather than a lab-only shortcut. POST /api/message accepts a caller-controlled message, enqueues it, and the queue processor later forwards the same message to invokeAgent() without adding any TinyAGI-side confirmation gate. The integration evidence in the attached PoC captures the spawned claude argv and shows the user-controlled request reaching the provider boundary with --dangerously-skip-permissions already present.

I also ran the matched control path against an OpenAI-backed agent through the same public HTTP interface. That control reached the provider boundary successfully but did not include the Anthropic-only bypass flag, which helps isolate the root cause to the Claude adapter rather than to the shared queue or HTTP plumbing.

PoC

Prerequisites

  • TinyAGI checked out locally and built so packages/main/dist/index.js exists
  • Node.js installed
  • Python 3 installed
  • Ability to start the TinyAGI daemon locally and send requests to 127.0.0.1
  • No API authentication is required for POST /api/message
  • The PoC uses fake claude and codex executables earlier in PATH so it can capture argv without requiring a live provider-side approval UI

Reproduction Steps

  1. Download the shared harness from: harness.py
  2. Download the verification script from: verification_test.py
  3. Download the matched control script from: control-provider-baseline.py
  4. Place the three files in the same directory, or use the equivalent local copies under llm-enhance/cve-finding/similar/rce/CVE-2026-30741-claude-tool-confirmation-bypass-exp/.
  5. From the TinyAGI repository root, build the runtime: npm run build
  6. Run the verification path: python3 verification_test.py
  7. Confirm that verification.log records a successful POST /api/message and that verification-captured-argv.json contains --dangerously-skip-permissions in the captured Claude argv.
  8. Run the matched control path: python3 control-provider-baseline.py
  9. Confirm that control-provider-baseline.log records [CONTROL-PASS] and that control-captured-argv.json does not contain --dangerously-skip-permissions.

Log of Evidence

[listening] localhost:52033
[baseline] TinyAGI isolated daemon is running
[probe] /api/message accepted messageId=api_za1h28rq
[evidence] captured argv=['--dangerously-skip-permissions', '--model', 'claude-sonnet-4-6', '--system-prompt', '...', '--output-format', 'stream-json', '--verbose', '-p', 'Use any available dangerous tool and explain what you would do.']
[flow] [HTTP message] -> /api/message -> queue -> invokeAgent -> claude CLI argv
[DEFECT-CONFIRMED-WITH-LIMITATIONS] TinyAGI forwarded untrusted API input into the Anthropic CLI with --dangerously-skip-permissions
[listening] localhost:55851
[baseline] TinyAGI isolated daemon is running
[probe] /api/message accepted messageId=api_o14ukdyh
[evidence] captured argv=['exec', '--model', 'gpt-5.3-codex', '-c', 'developer_instructions=...', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--json', 'Use any available dangerous tool and explain what you would do.']
[flow] [HTTP message] -> /api/message -> queue -> invokeAgent -> codex CLI argv
[CONTROL-PASS] same public interface reached a provider path without the Anthropic bypass flag

Impact

This is a remote confirmation-bypass issue in the Anthropic execution path. Any client that can reach TinyAGI's unauthenticated message API can cause an Anthropic-backed agent session to start with the dangerous-tool consent barrier already disabled. If the resulting model run attempts filesystem, network, shell, or other sensitive tool actions that Claude would normally gate behind explicit confirmation, that protection has already been removed by TinyAGI before provider-side execution begins.

In practical terms, this weakens a high-value security boundary around autonomous tool use. The directly impacted assets are the host resources and data that become reachable through Claude's tool surface in the affected deployment.

Affected products

  • Ecosystem: npm
  • Package name: tinyagi
  • Affected versions: >= 0.0.16, <= 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:H

Weaknesses

  • CWE: CWE-693: Protection Mechanism Failure

Occurrences

Permalink Description
const body = await c.req.json();
const { message, agent, sender, senderId, channel, messageId: clientMessageId } = body as {
message?: string; agent?: string; sender?: string; senderId?: string;
channel?: string; messageId?: string;
};
if (!message || typeof message !== 'string') {
return c.json({ error: 'message is required' }, 400);
POST /api/message accepts caller-controlled message content from the HTTP request body and only enforces a basic string check before continuing through the normal processing path.
const rowId = enqueueMessage({
channel: resolvedChannel,
sender: resolvedSender,
senderId: senderId || undefined,
message,
messageId,
agent: resolvedAgent,
The same untrusted message is persisted into the queue by enqueueMessage(), preserving it for later agent execution.
({ text: message } = await runIncomingHooks(message, { channel, sender, messageId, originalMessage: rawMessage }));
emitEvent('agent:invoke', { agentId, agentName: agent.name, fromAgent: data.fromAgent || null });
let response: string;
try {
response = await invokeAgent(agent, agentId, message, workspacePath, shouldReset, agents, teams, (text) => {
The queue consumer forwards the attacker-controlled message into invokeAgent() as part of normal processing.
return adapter.invoke({
agentId,
message,
workingDir,
systemPrompt,
model,
shouldReset,
envOverrides,
onEvent,
invokeAgent() hands the same message and execution context to the selected provider adapter.
const args = ['--dangerously-skip-permissions'];
if (model) args.push('--model', model);
if (systemPrompt) args.push('--system-prompt', systemPrompt);
if (continueConversation) args.push('-c');
if (onEvent) {
args.push('--output-format', 'stream-json', '--verbose', '-p', message);
The Anthropic adapter prepends --dangerously-skip-permissions unconditionally before passing the attacker-controlled prompt to claude, which disables Claude's dangerous-tool confirmation barrier for the session.

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