Static-Analysis Confidence Gap · 5 of 10L3data science
Modeling Agent-Specific Sources, Sinks, and Trust Boundaries
Static analysis ships a library of sources and sinks for conventional software; an agent needs a new, reusable catalog because its untrusted inputs and dangerous outputs are ones that library never named.
Abstract
A taint analysis is only as good as its inventory of where untrusted data enters, where it does harm, and where trust changes hands. That inventory ships with every analyzer — for conventional software. This piece argues that an AI agent introduces a distinct set of sources, sinks, and trust boundaries that the built-in library does not contain, and that supplying them as a structured, reusable catalog is what lets static analysis reason about an agent at all. The contribution is a taxonomy of agent endpoints — prompts, retrieval, tool results, memory, and inter-agent messages as sources; tool execution, external effects, credential access, identity delegation, and memory writes as sinks; the agent perimeter, tool-server, cross-agent, and memory boundaries as trust boundaries — together with the discipline for making the catalog accurate, composable, and maintainable. The consequence is a concrete, product-neutral account of the modeling work that must precede any meaningful clean report on an agent, and an honest marking of where even a complete catalog runs out.
Every taint analysis begins from a list: these locations are sources of untrusted data, these are dangerous sinks, and these transforms remove taint. The list is not glamorous, but it is the ground truth the whole analysis stands on — get it wrong and no amount of clever propagation helps, because the analyzer is looking for the wrong things in the wrong places. Analyzers ship a good list for conventional software: a web request parameter is a source, a database query is a sink, an escaping routine is a sanitizer. An AI agent is built from endpoints that list never anticipated. A retrieved document is a source; a tool invocation is a sink; the perimeter where the agent hands work to a separate tool server is a trust boundary. This piece is the catalog that has to be written before a scanner can see an agent at all — and the discipline for writing it well.
Why the built-in catalog does not fit an agent
Fix the three terms. A source is a location where untrusted data enters the program; a sink is a location where data arriving tainted can cause harm; a trust boundary is a line the analysis draws where the trustworthiness of data or authority changes, and at which controls are expected. Conventional analyzers ship a rich library of all three, tuned to the shapes ordinary software takes — network inputs, deserialization, query execution, command invocation, file paths.
That library encodes an implicit architecture: data arrives over well-known protocols, moves through code the analyzer reads, and exits through a fixed set of dangerous APIs. An agent keeps some of this substrate but adds a layer the library never modeled. Its most important untrusted inputs are not network parameters but content the agent was designed to consume — a ticket, a retrieved page, a tool's result, a memory of a past turn. Its most consequential operations are not raw system calls but tool invocations chosen from model output. And its trust boundaries are not the classic network perimeter alone but new lines around tools, other agents, and persistent memory.
The gap is therefore not that the analyzer is weak but that it is aimed at the wrong endpoints. Point a conventional source-sink library at an agent and it will faithfully find the conventional bugs in the substrate and be blind to the agentic ones, because it has no entry for a retrieved document as a source or a tool call as a sink. Supplying those entries — as a reusable, structured catalog — is the modeling work this piece specifies.
Agent sources: where untrusted data enters
The source side of the catalog is broader than intuition suggests, because in an agent almost everything the model reads can be attacker-influenced. The user prompt is the obvious one, but it is not the dangerous one; the dangerous sources are the ones a team forgets are untrusted. Retrieved content — a document, a web page, a database row pulled into context — is attacker-influenced whenever an adversary can write to what is retrieved, which is the whole premise of indirect prompt injection.
Tool results returned into context are sources: the output of a search, an API call, or a sub-tool re-enters the model as text that an attacker may have shaped. Agent memory is a source with a twist — a value written in one turn and read in a later one carries taint across the boundary of a single execution, so the read side of memory must be a source even though the write happened elsewhere. Inter-agent messages are sources when one agent consumes another's output, since a compromised or manipulated peer becomes an injection channel. Even environment and configuration read at runtime can be sources where an attacker can influence them.
The discipline here is to mark a source by whether an adversary can influence it, not by whether it looks like input. A retrieved wiki page does not resemble a network parameter, but if an attacker can edit the wiki it is exactly as dangerous, and the catalog must say so. Under-marking sources is the quiet way an agent taint analysis becomes unsound: a flow that begins at an unmarked source is invisible from its first step.
Agent sinks: where tainted data does harm
The sink side names the operations where a tainted, instruction-state value becomes a real-world effect. The central one is tool execution: any call the agent can make that touches the outside world — sending a message, writing a file, issuing a request, running code — is a sink, because a tool driven by an injected instruction is the payload of nearly every agent attack. These are not the raw system calls of the conventional library; they are the framework's tool-invocation points, which is why they must be modeled explicitly.
Several sinks are specific to the agentic setting. Credential and token access is a sink because a flow that reaches the agent's secrets can exfiltrate or misuse the authority they carry. Identity delegation — the agent acting on behalf of a user or assuming a role to call a downstream service — is a sink because tainted influence over who the agent acts as is an authority attack, not merely a data one. Memory writes are sinks that pair with memory-read sources to carry taint across turns, and tool registration itself can be a sink where an agent that can add or modify its own tools at runtime is induced to install a dangerous one.
The marking discipline mirrors the source side but inverts the risk of error. Where under-marking sources causes silent misses, under-marking sinks causes the same, while over-marking sinks causes noise. A catalog that labels every function call a sink is useless; one that labels the genuine external-effect, authority, and persistence points is precise. Saltzer and Schroeder's principle of least privilege is the guide: the sinks that matter most are the ones carrying the most authority, and those are exactly where a tainted flow is most worth finding.
| Sink | Why it is a sink | Marking note |
|---|---|---|
| Tool execution | External effect driven by model output | Mark the framework's invocation point, not raw syscalls |
| Credential / token access | Exfiltration or misuse of authority | Reaching secrets is harm even without a further call |
| Identity delegation | Tainted control over who the agent acts as | An authority attack, not only a data one |
| Memory write | Carries taint into a later turn | Pairs with a memory-read source across turns |
| Tool registration | Agent installs or alters its own tools | A sink only where runtime self-modification exists |
Trust boundaries: where trust changes hands
Sources and sinks say where data enters and exits; trust boundaries say where its trustworthiness or the agent's authority changes, and they are what make the catalog composable rather than a flat list. The agent perimeter is the first boundary: the line between the outside world and the agent's own process, crossed by every source and sink already named. But an agent has interior boundaries the conventional model lacks, and marking them is what lets the analysis reason about partial trust.
The tool-server boundary is the line between the agent core and a separate tool-providing process, increasingly reached over a protocol; data and instructions crossing it change hands between components with different trust, and a value returning across it is a source while a call going out is a sink. The cross-agent boundary is the line between one agent and another, where a peer's output must be treated as untrusted on entry. The memory boundary is subtler: it separates one turn's execution from the next, and a value crossing it via persistent storage should be re-tainted on the far side, because the write-time context that made it seem safe is gone by read time.
Marking these boundaries follows the zero-trust instinct that NIST's guidance formalizes: do not trust a value or an authority because of where it sits, but verify at each boundary it crosses. For the catalog this means every boundary is a place to attach a control expectation — a verifiable guard on the tool-server return, an authorization check on the cross-agent message, a re-tainting rule on the memory read. Boundaries are where the earlier sanitizer-placement rule lives, and where a clean report earns or fails to earn its meaning.
Making the catalog reusable and composable
A catalog written once for one agent is worth little; the value is in a structured, shareable form that composes across systems built on the same framework. The unit is a model: a small, declarative statement of an endpoint, its role — source, sink, or boundary — and its kind, a tag naming the threat category it belongs to, such as external-effect, authority, or persistence. Gathered into a pack keyed to a framework and a version, these models become an artifact a team maintains and shares, the way library models are shared for conventional dependencies.
Two properties make the pack composable. First, the kind tag lets the analysis reason about categories rather than individual endpoints, so a new tool declared with kind external-effect inherits the same treatment as every other without a bespoke rule. Second, extension points let a team add their own tools, memory schema, and boundaries without editing the framework's base models, so a shared pack for a framework composes with a private pack for one application. The result is a layered catalog: the framework's endpoints modeled once for everyone, the application's endpoints modeled once locally, and the two combined at analysis time.
This is the same models-as-data discipline that reconnects severed edges, applied to the endpoint inventory rather than the call graph. Because the catalog is data, its coverage is measurable: one can count the fraction of an agent's real endpoints that have a model and treat the remainder as known gaps. A pack is a hypothesis about the agent's shape, and like any hypothesis it is only useful when its scope is explicit and its accuracy is maintained.
A worked catalog fragment
Made concrete for a support agent that reads tickets and can send email, the catalog fragment is short and legible. The ticket text and any retrieved knowledge-base article are sources of kind untrusted-content. The model's own output is a source too, tainted whenever tainted content reached it, a rule that belongs in the catalog so the analysis never treats a model response as fresh and clean. The send_email tool is a sink of kind external-effect; the store into conversation memory is a sink of kind persistence, paired with the later memory read as a source.
The trust boundaries in the fragment are the agent perimeter, crossed by the ticket coming in and the email going out, and — if the email tool lives in a separate service — a tool-server boundary, at which a verifiable guard on the outgoing arguments is expected. Each boundary carries a control expectation, and the catalog records not just that the boundary exists but what guard should sit on it, so a missing guard is a visible gap rather than an unstated assumption.
The value of writing this down is that it converts a vague sense of the agent's attack surface into a precise, checkable inventory. With the fragment loaded, the analysis looks for exactly the right shape: a flow from a ticket or retrieved article, through the model, to send_email or a memory write, with no verifiable guard on the boundary it crosses. That shape is the agent's real risk, and it was invisible to the built-in library because none of these endpoints were in it.
# sources
ticket.text ; role=source ; kind=untrusted-content
kb.article ; role=source ; kind=untrusted-content
model.output ; role=source ; kind=untrusted-content # tainted if input was
memory.read ; role=source ; kind=persistence
# sinks
tools.send_email ; role=sink ; kind=external-effect
memory.write ; role=sink ; kind=persistence
secrets.get ; role=sink ; kind=authority
# boundaries (each carries a control expectation)
agent.perimeter ; role=boundary ; guard=required
tool_server.return ; role=boundary ; guard=verifiable-allowlist+schemaGetting the catalog right, and its limits
A catalog is a precision instrument, and the two ways to misuse it are symmetric. Over-broad sources or sinks flood the analysis with paths that are not real risks, training reviewers to ignore it; missing sources or sinks make genuine flows invisible, which is worse because it is silent. The remedy is the classification discipline: mark a source only where an adversary can truly influence it, a sink only where a tainted value truly causes harm, and a boundary only where trust truly changes — and review the catalog independently, since an endpoint list is exactly the kind of artifact whose errors hide in plain sight.
The sanitizer caveat from the propagation rules carries straight over: the catalog may declare guards on boundaries, but only verifiable transforms count as guards. A boundary annotated with a control expectation the code does not actually enforce is a false comfort, so the catalog should record the expectation and the analysis should verify it is met, flagging any boundary crossed without its guard. This keeps the catalog honest — it is a statement of what should be true, checked against what is.
The honest limit is that even a complete, accurate catalog does not reach inside the model. It marks the endpoints and boundaries around the model perfectly and still cannot decide whether the model, at runtime, turns a tainted source into a followed instruction. The catalog's job is to make the structural risk visible — every unguarded source-to-sink path across a real boundary — and to hand the behavioral question to runtime. Its coverage number is the measure of how much of the agent's surface the analysis can see; the uncovered remainder, and the model itself, are where other assurance must carry the weight.
From catalog to a meaningful clean report
The payoff of the catalog is what it does to the meaning of a clean report. Without it, silence on an agent is nearly empty, because the analysis was never looking for agentic flows — it had no entry for the ticket source or the send_email sink. With it, silence acquires weight in proportion to coverage: on the endpoints and boundaries the catalog models, a clean report is a real statement that no unguarded path was found; on the endpoints it omits, the report says nothing, and the coverage number is what tells the two apart.
This makes the catalog the precondition for every later claim in this line of work. Measuring an analyzer's recall on agent flows requires a catalog to define what a flow even is; modeling framework glue requires a catalog of endpoints for the glue to connect; propagating taint across the model requires the sources and sinks the model sits between. The endpoint inventory is the shared foundation, and building it deliberately — reusable, composable, measured — is the unglamorous work that makes the rest possible.
The closing point is that the catalog is where an agent's security first becomes legible to a scanner, and where its illegible remainder is honestly marked. Everything the catalog names, the analysis can search and a clean report can speak about; everything it cannot name — chiefly the model's own behavior — is handed onward with its boundary drawn. A team that writes this catalog well has given static analysis the best possible view of its agent, and has drawn, in the same act, the exact line past which other controls must see for it.
Key takeaways
- A taint analysis stands on its inventory of sources, sinks, and trust boundaries; the built-in library encodes conventional software and omits an agent's endpoints entirely.
- Agent sources are broader than intuition: retrieval, tool results, memory reads, and peer messages are all attacker-influenceable, and the model's own output is tainted when its input was.
- Agent sinks include tool execution, credential and token access, identity delegation, memory writes, and runtime tool registration — framework operations, not raw system calls.
- Agents have interior trust boundaries — tool-server, cross-agent, and memory — where trust changes and controls are expected, not just an outer perimeter.
- A reusable catalog is a pack of (endpoint, role, kind) models, keyed to a framework and version, with extension points so shared and private packs compose; its coverage is measurable.
- Even a complete, accurate catalog stops at the model boundary: it makes the structural risk checkable and hands the behavioral question of whether the model is steered to runtime.
Practitioner Toolkit
Copy-paste, strictly defensive artifacts you can use today. Nothing here attacks a real system.
Work through each row before trusting a clean report on an agent.
- Mark as sources: user prompt, retrieved content, tool results, memory reads, peer messages, and the model's own output.
- Mark as sinks: tool execution, credential/token access, identity delegation, memory writes, and runtime tool registration.
- Mark trust boundaries: agent perimeter, tool-server, cross-agent, and memory — each with the control expected on it.
- Tag every model with a kind (untrusted-content, external-effect, authority, persistence) so rules reason by category.
- Mark a source only where an adversary can influence it and a sink only where a tainted value truly causes harm.
- Record each boundary's expected guard and verify it is actually enforced — an unenforced expectation is false comfort.
- Measure coverage: the fraction of real endpoints modeled; treat the remainder as explicit gaps.
Vendor-neutral shape of a shared framework pack plus a private application pack; adapt to your analyzer's format.
pack: framework-agent-base@1.x # shared, keyed to framework + version
source: tool.result ; kind=untrusted-content
source: memory.read ; kind=persistence
sink: tool.invoke ; kind=external-effect
boundary: tool_server ; guard=required
pack: my-support-agent (extends base) # private, application-specific
source: ticket.text ; kind=untrusted-content
sink: tools.send_email ; kind=external-effect
boundary: perimeter ; guard=verifiable-allowlist+schemaThe shortest catalog that makes a clean report mean something on an agent.
- List the top three untrusted sources and the top three consequential sinks for your agent.
- Draw the boundaries between them and note the guard each should carry.
- Model those endpoints, then confirm a known-vulnerable flow surfaces end to end.
- Publish the coverage number next to any clean report so its scope is explicit.
Glossary
- Source
- A location where untrusted data enters the program; for an agent, any endpoint an adversary can influence, including retrieval, memory, and tool results.
- Sink
- A location where data arriving tainted causes harm; for an agent, tool execution, credential access, identity delegation, and memory writes.
- Trust boundary
- A line where the trustworthiness of data or the agent's authority changes and a control is expected, such as the tool-server, cross-agent, or memory boundary.
- Kind
- A tag on a model naming the threat category of an endpoint — external-effect, authority, persistence, untrusted-content — so rules can reason by category.
- Model pack
- A structured, versioned collection of endpoint models keyed to a framework, shareable and composable with an application's private models.
- Memory boundary
- The line between one turn's execution and the next; a value crossing it via persistent storage should be re-tainted because its write-time context is gone.
- Coverage
- The fraction of an agent's real endpoints that have a model; the uncovered remainder is a known gap the analysis cannot see.
References
- OWASP Agentic AI — Threats and Mitigations
- Saltzer & Schroeder, The Protection of Information in Computer Systems (Proc. IEEE, 1975)
- NIST SP 800-207, Zero Trust Architecture
- Livshits & Lam, Finding Security Vulnerabilities in Java Applications with Static Analysis (USENIX Security 2005)
- MITRE Common Weakness Enumeration (CWE)
- NIST SP 800-218, Secure Software Development Framework (SSDF) v1.1