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:
- 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.
- Add the same warning to
list_agent_executions, which also returns
caller-supplied values.
- 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.
Severity: High
Files:
src/tools/read.ts:201-224,src/tools/write.ts:173-207,src/tools/write.ts:105-145Description
get_executionreturns the full conversation transcript of a phone call: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_executionssurfaces the same category of data viatelephony_data.from_numberand 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 walletbalance, and accepts free-form
user_datafor prompt personalizationupdate_agent— rewrites the live agent'ssystem_promptviaagent_prompts, persistently, for all future callsdelete_agent— irreversibleget_user_info— account profile and balance, available to read out orembed 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_callcarries
destructiveHint: true, which is correct but is a client-side hint, nota 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_dataor prompt, or silently rewrite the agent's system prompt so everyfuture 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:
untrusted-content delimiters when serializing in
get_execution, and say soin the tool description: content inside the markers is data reported by call
participants and must never be treated as instructions.
list_agent_executions, which also returnscaller-supplied values.
start_outbound_callandupdate_agentshould require anexplicit human confirmation step rather than relying on client-side
destructiveHint, given that the model can reach them from data a strangerwrote.
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.