Prompt Injection Through Tool Output
Prompt injections hidden in tool outputs exploit the gap between input screening and action execution, as no existing security screen sees both contexts simultaneously.
Deep dive
- Tool results are often treated as 'trusted' if they come from internal systems, but they are attack vectors if an outsider writes to the fields (e.g., PR comments).
- Conventional screens isolate segments: input filters read data, action filters check intent, but neither links the tainted data to the resulting call.
- The 'Precedent Gap' signal identifies an agent making a tool call or using arguments it has never used in previous execution cycles.
- Effective defense requires profiling agent behavior (e.g., eBPF) to baseline normal tool usage and block anomalies at the execution layer.
Decoder
- Precedent Gap: An incident where an agent attempts a tool action that does not exist in its historical behavioral baseline.
Original article
Prompt Injection Through Tool Output Is Two Events (Your Screens Read One)
Key takeaways
- Why does OWASP treat tool output as untrusted? Because an outsider can write to it. OWASP's prompt injection prevention cheat sheet groups tool output with RAG documents, web pages, and email bodies as content that has to be screened before the model sees it, and the thing those four share is that someone outside your trust boundary composed the text. A ticket body, a CRM note, and a PR description all qualify even when the system returning them is one you own.
- Why do OWASP's screens pass a tool-result injection? Each screen holds one moment of the agent loop. Input screening reads the result as content, action screening reads the following call as an authorized operation, and both are correct in isolation. The injection is the relationship between the two moments, and no screen sees both.
- What does a successful tool-output injection produce that you can observe? A tool call with no precedent in that agent's own history, issued in the same turn as the result. The tool is correct, the schema validates, the session is authorized, and the destination, path, or argument has never appeared in months of that agent's execution. That precedent gap is the signal, and only a system holding that agent's history can read it.
Tool output is untrusted because your own systems produce it.
That is the part of the OWASP guidance that never makes it into a deployment. The label goes on web pages and email bodies, where an outsider obviously wrote the text. It never goes on the ticket store, the CRM, or the repo, because those are yours. The attacker does not care whose system it is. He cares which field takes free text: the ticket body, the opportunity note, the PR description.
So the result arrives from a trusted source, passes input screening as content, and the call that follows it passes action screening as an authorized operation. Each screen reads one of them. The attack is both.
This piece is about the second read, and what it takes to see it.
Untrusted tool output is generated by the systems you trust
Tool output is any text a tool returns into the agent’s context after a call. It sits in the same screening bucket as RAG documents, web pages, and email bodies in OWASP’s prevention guidance, which also names forged tool output as an agent-specific attack pattern. It is untrusted for one reason: someone outside your trust boundary can put words into the field. The system that delivered it does not change that.
Tool output fails that test far more often than the tool list suggests. The tool is Jira, and the field is a ticket body a customer typed. The tool is GitHub, and the field is a pull request description an outside contributor wrote. The tool is Salesforce, and the field is an account note pasted from an inbound email. The tool is Postgres, and the row came from a web form.
Tool output carries a trust label that was assigned to the system, and assigning it there is the reasonable move: the system is what procurement reviewed and what the platform team integrated. The attacker never touches the system. He targets the one field inside it that accepts free text, and that field returns to your agent with the system’s reputation attached.
Tool output is the one injection channel that never crosses your perimeter. A poisoned web page arrives from outside and gets the treatment outside content gets. A poisoned ticket was already inside when the agent asked for it. OWASP’s LLM01 entry calls indirect injection a case of the model accepting input from an external source, and a ticket store is external to the model while sitting inside your network.
Re-label your tool inventory by field
Start with one agent. The same tool is trusted for one agent and untrusted for another, because what matters is which of its fields that agent actually reads.
For that agent, list every tool it can call. For each tool, list the return fields that carry free text. Numbers, enums, and structured identifiers are out of scope. For each free-text field, write down who can put words into it. The answer is the label. A field only your own service writes is trusted. A field a customer, a contributor, a vendor, or a web form can write is untrusted, and the tool that returns it is an injection channel for this agent regardless of what the tool is called.
| System | Return field | Who writes it | Label |
|---|---|---|---|
| Ticketing | ticket body, comments | any customer | untrusted |
| Ticketing | status, priority, assignee | your team | trusted |
| Knowledge base | article body | your team, contractors | trusted, review contractors |
| CRM | account notes, activity log | sales reps, inbound email parser | untrusted (parser) |
| Order database | shipping notes | customer at checkout | untrusted |
| Order database | order ID, SKU, amount | your service | trusted |
The label alone does not rank anything. Rank by pairing an untrusted field with a high-privilege tool inside the same agent. The support agent that reads ticket bodies and can only post replies is a disclosure risk. The support agent that reads ticket bodies and can query the customer table, issue a refund, or open an internal link is the one where a coerced call changes state. Sort your agents by the number of untrusted fields multiplied by the number of state-changing tools, and baseline from the top of that list.
Each OWASP screen holds one moment of the loop
OWASP’s prompt injection prevention cheat sheet places three screens and one architecture around the model: input screening, output screening, action screening, and the dual-LLM pattern. Each reads one point in the agent loop.
Input screening reads the tool result before the model sees it. The cheat sheet says to run retrieved context and tool output through a classifier, and it says plainly that pattern filters miss indirect injection in untrusted content. So the screen is a model judging adversarial prose, and its position is fixed: it sees the result, alone, before any decision has been made.
Output screening reads the model’s response after the decision. It catches system-prompt leakage, exfiltration markup, and policy-violating text on the way out. Its position is after the reasoning and before the user, and it sees what the model said. What the model did is outside its position.
Action screening reads the proposed tool call against the user’s original intent. The cheat sheet’s design deliberately withholds the intermediate context from this screen, so that an action drifting because of an injected instruction can be refused on intent alone. Its position is the call, isolated from the result that caused it.
The injection lives between the result and the next call
A tool result lands in the context window, and within the same turn the agent issues its next call. Those are two events with a few hundred milliseconds between them, and the injection is what happens across that gap.
The result arrives as the most recent thing the agent has read, and recency is why it works. A description is one line competing with every other tool’s line. A result is the last thing in the window when the agent decides what to do next, and it carries the standing of data the agent asked for. The split is measurable.
The call the agent then makes is unremarkable in every respect but one. It uses a tool the agent is registered to use. Its schema validates. Its session is authorized. What differs is a destination, a path, a table name, or an argument value that this agent has never used before.
A coerced call has no precedent in the agent’s own history
Seeing the pair takes three things: a baseline, a deviation rule, and an enforcement path that starts before anything is blocked.
The baseline is a record of what one specific agent does when it is not being coerced. For this attack it has to carry four fields: which tools the agent invokes, which argument values it uses against them, the order it calls them in, and the file, process, and network activity each call produces underneath.
The deviation is a call in the turn after a tool result that has no precedent in that baseline. Either the tool itself is one this agent has never called, or the tool is familiar and the argument is new. When the deviation fires, the result event that preceded it and the call that followed it land on one timeline as a single Attack Story, so the responder reads one incident with both halves on it.
The enforcement path runs from audit to enforce. In audit, deviations are recorded against the baseline and nothing is blocked, which is how the policy gets proven safe against production traffic before it can break anything. In enforce, the deviating call is stopped. Credential isolation closes the loop from the other side, so a coerced read of a credential path that clears every other check returns nothing an attacker can use.
Screen the result, then instrument the call that follows it
The screens stay. They remove the opportunistic volume, and every result they clear is one fewer the baseline has to adjudicate. What they cannot carry is the targeted payload written for your stack, because that payload passes input screening as content and produces a call that passes action screening as authorized.
The pair is what the attacker cannot avoid producing. To finish, the injected instruction has to make a real tool do something real, and doing something real leaves a call against a history the attacker never saw.
Three actions, in order. Re-label your tool inventory by field, and rank agents by untrusted fields paired with state-changing tools. Baseline the agents at the top of that list. Run in audit until the deviation rate settles, then enforce.
Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi