handoff series · exploit 04 of 08

A4 — Rogue agent registration

This one never touches the prompt. Register a malicious agent under a trusted name, and the next legitimate handoff routes into attacker code that reads the secrets file.

A1 through A3 all abused the same thing from different angles: an LLM acting on untrusted text. A4 is the outlier that proves the surface is bigger than the prompt. It sends no markers, needs no injection, and never speaks to the model. It attacks the layer beneath the agents: the registry that maps a name like OpsAgent to the code that runs when someone hands off to it. Poison that map and every legitimate handoff becomes an attacker's entry point.

Build
flip it: the console below switches between exploited and blocked

What the buttons do: click VULN and every console below shows the attack succeeding on the vulnerable build; click FIXED and the same consoles show it blocked on the hardened build. It switches in place: nothing reloads, so you can flip back and forth and compare the exact same step exploited vs. defended.

the seamBoundary ❾ — the registry

Agents are resolved by name. A handoff to OpsAgent is really a lookup: "find the agent registered under that name, run it." The registry is trusted implicitly: nobody re-checks, at handoff time, that the OpsAgent now in the table is the same OpsAgent the operator deployed. If registration is open, an attacker can register their own agent under an existing trusted name and inherit all the trust that name carries. It is DNS cache poisoning for agents.

the payloadOverride the name

The attacker registers a malicious agent under the name OpsAgent, overriding the real one, so the next legitimate handoff routes into their code and reads secrets.env. The fix is an authenticated registry that refuses to override an existing name without an operator token:

A4 · VULN · exploited
[attacker]    register rogue 'OpsAgent': ok=True
[AccountAgent] handoff -> OpsAgent: ok             # now the attacker's agent
[OpsAgent]    tool read_file('secrets.env') -> OPENAI_API_KEY=sk-REDACTED-LIVE-KEY
A4 · FIXED · blocked
[attacker]    register rogue 'OpsAgent': ok=False (DENIED: cannot override existing agent)
[AccountAgent] handoff -> OpsAgent: ok             # the REAL OpsAgent
[OpsAgent]    tool issue_refund('u1001','100') -> REFUND_ISSUED account=u1001 amount=100   # benign

the fixAuthenticate the registry

Registration is a privileged operation, so it must be authenticated. Overriding an already-registered name requires an operator token; without it, the override is refused and the deployed agent stands:

authed_registry — no silent override
if self.policy.authed_registry and name in self.agents:
    if not operator_token_valid(token):
        return Denied("cannot override existing agent")
# first registration is fine; overriding a trusted name is not
A4 and A7 are cousins

A4 forges an internal identity; A7 forges a cross-organization one. The mistake is identical: trusting a name without verifying who actually stands behind it. But the trust anchor differs: A4's is an operator token you hold internally, A7's is a cryptographic signature from an issuer you chose to trust. Same lesson, two boundaries.

threat modelA4, seen from above

Asset at risksecrets.env: API keys and the DB password. This is the only exploit in the set that reaches the secrets file directly. Property lost: confidentiality (and, downstream, everything those keys unlock).
STRIDE categorySSpoofing: the attacker impersonates a privileged agent, which then yields IInformation disclosure of the secrets.
Trust boundary the registry / federation boundary, where an agent identity is admitted to the mesh.
Adversary & reachAn insider or a weak registration path: anyone who can write to the registry. Note the shift: unlike A1–A3, the channel is not the chat message; it is the control plane.
Attack-tree branchA4 is a goal in its own right (exfiltrate the secrets file), orthogonal to the SSN tree, which is exactly why a threat model must enumerate assets, not just attacks.

deep diveThe adversary you forget: not everyone comes through the chat box

A4 is the reason the series' threat model lists adversaries by the channel they control, not by a single "the attacker" abstraction. The important, counter-intuitive thing about the full list is that none of these adversaries need to touch the model; each just needs a way to get bytes into a surface the runtime reads. A4 is the one whose surface is the control plane rather than the conversation.

AdversaryChannel they controlBoundaryExploits
External userthe chat messageA1 · A2 · A3
Poisoned knowledge sourcea retrieved document; no direct access neededA5 · A6
Malicious / over-eager partner orgits own Agent CardA7 · A8
Insider / weak registration paththe agent registryA4

Threat-model a mesh only through the prompt and you will build a beautiful input filter and still ship A4 wide open. The registry is a trust boundary; treat writes to it like writes to production config, because that is what they are.

in the wildWhere A4's seam lives in real frameworks

Service discovery / agent registries

Any system where agents are resolved by name at runtime has this seam. Require authenticated registration and treat name-override as a privileged, audited event, never a silent upsert.

MCP server configuration

An MCP server is a named capability provider. Pin which servers a client trusts; don't let an untrusted process register or shadow a server name that tools resolve against.

Plugin / tool marketplaces

This is OWASP LLM03 (Supply Chain): a component admitted by name inherits trust. Verify provenance at install and at resolve time, not just once.

catching it in prod

Every registration and every override is a security event: log it with the identity that made the change and alert on any override of an existing agent name. A deployed agent's implementation hash changing without a deploy is your tripwire.