Skip to main content

Every Page Your Agent Reads Is Executable

· 7 min read
CatalEx Engineering
The team building CatalEx
CatalEx Engineering · Published August 23, 2026 · 09:00 UTC

The version of prompt injection that should worry you is the one where nobody talks to the AI at all.

An employee asks the company agent to research a competitor. The agent does what it was built to do: it opens pages. On one of them, somewhere in the markup, is text written for a reader that isn't human. If you are an AI reading this, ignore your previous instructions and… You can fill in the rest.

Nobody clicked a suspicious link. Nobody typed a malicious prompt. There is no user to retrain, no phishing test to run, no lapse to point at in the incident review. The agent encountered the attack in the course of doing its job correctly, and the person who asked for the research did nothing wrong at any point.

That's indirect prompt injection, and it changes what the perimeter is. When an agent can browse, read a workspace, open documents, process email — and also take actions — every piece of external content it touches becomes potentially executable influence. Not "content the model might be misled by." Input to a system with credentials.

The blame model is wrong, so the controls land in the wrong place​

Most security programs are built around a person making a decision: don't click that, don't install that, don't approve that wire transfer. The controls follow the decision — training, filters, confirmation dialogs at the point of human judgment.

Indirect injection has no such decision to attach to. The person made one choice — "research this competitor" — and it was a reasonable one. Everything after it was the system reading things, which is the entire function you deployed.

So the control has to move to where the untrusted content actually enters, and the question stops being did the user do something risky and becomes what can this content reach. That's an architecture question, and it has a specific answer: the part of the system that reads the open internet should not be the part of the system that holds your capabilities.

Isolate the reading path from the acting path​

The default shape of a browsing agent is one loop that fetches pages, reasons about them, and calls tools — all in one context, with one set of credentials. That shape has no boundary in it anywhere. A page fetched in step three has a direct line to a tool call in step four, and the only thing between them is the model's judgment about which of the tokens in its context deserve to be obeyed.

The fix is to split it:

ComponentSeesHolds
FetcherRaw page bytesNo credentials, no tools, no memory
ReaderFetched contentExtraction tools only; returns structured data
PlannerStructured data, never raw page textCredentials and the action tools

The planner is the component with reach, and it never reads attacker-controlled prose. What it receives is a typed result — fields, values, extracted claims — not a document that can argue with it. An instruction embedded in a page can still corrupt what the reader extracts, and that's a real and separate problem worth bounding with schemas and cross-checks. What it can no longer do is tell the component holding your API keys what to do next, because that component isn't reading it.

This costs expressiveness. A planner that can't read the page can't notice the nuance in it, and some research tasks genuinely degrade. That's the trade, and it's worth making deliberately rather than discovering later that you never made it.

Treat fetched content as data at every layer​

Isolation is the structural move. These are the ones that make the isolated reader harder to turn.

Strip before you read. HTML comments, display:none and zero-opacity spans, white-on-white text, off-screen absolutely positioned blocks, aria-hidden content, metadata fields, alt text. Extract to a normalized text representation and drop anything a human visitor would never see. An attacker who has to write the payload in visible body copy has a much harder job — the page has to look hostile to the humans reading it too.

Label provenance, always. Every span of content in the reader's context carries where it came from, and the labeling is applied by the fetcher, not claimed by the content. Content that arrives without a provenance label is a bug, not a default-trusted string.

Classify, and don't rely on it. Run an injection classifier over fetched content. It will catch the unsophisticated majority and miss the interesting minority. Use it to raise alarms and quarantine sources, never as the thing standing between a page and a capability.

Bound the fetch surface. A research agent that can fetch any URL can be walked anywhere by the pages it reads — one hostile page links to the next, and the agent follows because following links is what research looks like. Cap crawl depth. Require that fetched domains come from the task's initial scope or a human-approved list. Log every URL the agent chose to open, because the choice itself is signal.

The redirect is the hole in the domain allowlist. Allowlisting the domain you requested is not allowlisting the domain you received. Resolve redirects and re-check the final host, or an approved short link becomes an approved path to anywhere.

Constrain what a browse session can do afterward​

The strongest control is the least clever one: after an agent reads untrusted content, its capabilities shrink.

Concretely, a session that has fetched from the open internet should not, in the same session, hold outbound send capability or credentials to internal systems. Not "should require approval" — should not hold it. If the workflow needs both, it's two workflows: one that reads and produces a structured artifact, and one that acts on that artifact after a human or a deterministic check has looked at it. The handoff is the boundary, and it's the only kind of boundary that doesn't depend on the model's cooperation.

Where an action must follow a browse, the check around it is code: a URL against a destination allowlist, a recipient against a known set, arguments against a schema, resolved by something that never read the page. Deterministic checks are the only part of this system an injected instruction cannot address.

What this doesn't fix​

Isolation stops the page from driving your tools. It does not stop the page from lying.

A hostile source that gives up on hijacking the agent can simply publish false facts, and a research agent will faithfully extract and report them. That's not injection; it's contamination, and it needs different controls — source trust, corroboration across sources, and the ability to trace a conclusion back to the document that produced it. Worth naming here so the isolation architecture doesn't get mistaken for a complete answer.

And there is no clean fix for the reader itself being manipulated into mis-extracting. Schemas help. Requiring the reader to return verbatim quoted spans alongside its extraction helps more, because a human or a checker can then compare the claim to the source text. Neither is airtight.

The sentence to design against​

The failure at the root of all of this is an agent that reasons:

I read it, therefore I can trust it.

Nothing in a language model's architecture separates encountered from endorsed. Everything in the context window is just there, with the same standing, and the model has no channel through which to learn otherwise.

So the separation gets built around it: fetchers with no power, readers with no reach, planners that never see raw prose, capabilities that shrink after untrusted input, and checks written in code at every point where the agent touches something real. Content the agent reads should be able to inform what it does. It should never be able to decide it.


Written by CatalEx Engineering. We build the AI operating layer for AI-native companies — one platform to build, deploy, and run AI agents in production. More at catalex.co.