Skip to content

Call transcripts are untrusted third-party text feeding money-spending tools #3

Description

@rajarshidattapy

Severity: High
Files: src/tools/read.ts:201-224, src/tools/write.ts:173-207, src/tools/write.ts:105-145

Description

get_execution returns the full conversation transcript of a phone call:

const execution = await bolnaFetch(
  `/executions/${encodeURIComponent(execution_id)}`,
  apiKey
);
return jsonResult(execution);

The response is serialized straight into the tool result with no wrapping, no
delimiting, and no marking. Half the content of any transcript is dictated by
whoever was on the other end of the line — a member of the public, not the
account owner. list_agent_executions surfaces the same category of data via
telephony_data.from_number and makes execution IDs easy to enumerate.

That untrusted text lands in the model's context inside a session that also
holds:

  • start_outbound_call — dials an arbitrary E.164 number, spends wallet
    balance, and accepts free-form user_data for prompt personalization
  • update_agent — rewrites the live agent's system_prompt via
    agent_prompts, persistently, for all future calls
  • delete_agent — irreversible
  • get_user_info — account profile and balance, available to read out or
    embed in a subsequent call

None of the eleven tool descriptions mention that transcript content is
untrusted, and no tool requires confirmation before acting. start_outbound_call
carries destructiveHint: true, which is correct but is a client-side hint, not
a gate.

Impact

This is the full lethal trifecta in one tool surface: untrusted input, access to
private data, and an outbound channel that can carry data off the system.

A realistic path: a caller ends a conversation with instructions addressed to
whatever will later read the transcript. The account owner then asks their
assistant something ordinary — "summarize yesterday's calls and follow up on
anything that needs it." The model reads the injected instructions as part of
the transcript and has, in the same session, everything needed to act on them:
place a call to an attacker-chosen number, read account data into that call's
user_data or prompt, or silently rewrite the agent's system prompt so every
future caller gets an attacker-controlled script.

The outbound-call path makes exfiltration especially direct — a phone call is an
egress channel that leaves no trace in the MCP client and is not subject to any
URL allowlist or network policy.

Suggested fix

Defense in depth, cheapest first:

  1. Wrap transcript and other caller-controlled fields in explicit
    untrusted-content delimiters when serializing in get_execution, and say so
    in the tool description: content inside the markers is data reported by call
    participants and must never be treated as instructions.
  2. Add the same warning to list_agent_executions, which also returns
    caller-supplied values.
  3. Consider whether start_outbound_call and update_agent should require an
    explicit human confirmation step rather than relying on client-side
    destructiveHint, given that the model can reach them from data a stranger
    wrote.

Note that no amount of prompt-level marking is a complete defense; the
structural mitigation is the confirmation gate on the tools that spend money or
mutate live agent configuration.

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