Most discussion of prompt injection is about the direct kind: a user types something adversarial and the model complies. That's real, it's well documented, and it's also the easier half of the problem.
The harder half is indirect injection, where the instruction arrives inside content the user never wrote.
The shape of the attack
An LLM sees one flat token stream. It has no reliable structural way to distinguish "this is my operator's instruction", "this is the user's question", and "this is a document the user pasted". Everything is text, and text that looks like an instruction tends to get treated as one.
So an attacker doesn't need to reach the user at all. They need to get text in front of the model. Some routes:
- A webpage the user summarises, with an instruction in a
<!-- comment -->or in white-on-white text. - A PDF or email forwarded to an assistant, carrying a line addressed to the AI rather than the reader.
- A document in a shared drive that a retrieval system will later pull into context.
- A code comment in a dependency, waiting for someone to ask an assistant to review the file.
The user does something entirely reasonable — "summarise this page for me" — and the model reads an instruction the user never saw.
Why it's harder than the direct kind
With direct injection, the adversarial text is the user's input. You can be suspicious of user input; that's a familiar security posture.
Indirect injection breaks that. The suspicious text is in a legitimate part of the request — the content the user is legitimately asking about. You cannot simply distrust pasted content, because pasted content is the entire point of the feature.
Worse, the payload is usually invisible in the surface the user sees. An HTML comment doesn't render. White text on a white background doesn't render. Zero-width characters don't render. From the user's point of view they pasted a normal document.
What we look for
Our indirect-injection detector doesn't try to decide whether text is "malicious" in the abstract — that's a losing game. It looks for instruction-shaped text in positions where instructions don't belong, plus the carriers attackers use to hide it.
Concretely, the patterns fall into a few families:
Instructions inside comment syntax. An HTML or markdown comment containing an instruction verb — ignore, disregard, system, you must, instead, do not tell, reveal. A comment is for humans reading source; a comment issuing orders to a model is not an accident.
Embedded role markers. A line beginning system: or assistant: in the middle of quoted content, followed by something imperative. Real conversations don't have a second system prompt buried at line 400 of a pasted document.
Text addressed to the model. Content that speaks directly to "the AI", "the assistant", "the model", or "the chatbot" and then tells it to do something. Documents written for people don't usually address the machine.
Hidden carriers. Zero-width characters, invisible Unicode, and encoded blocks are treated as signals in their own right. Content with no legitimate reason to contain a zero-width joiner mid-sentence is worth a closer look.
Each pattern carries a confidence rather than a verdict, and the policy decides what to do with it. In practice indirect injection warns and escalates rather than hard-blocking, because false positives here are annoying and the base rate is low.
What this does not solve
Semantic attacks with no structural tell. An instruction phrased as ordinary prose, in a position where prose belongs, with no comment syntax and no role marker, will not trip a pattern. This is where a classifier helps, and it's a large part of why we built the ML layer — though it stays switched off until a real model backs it.
The model's own susceptibility. We reduce what reaches the model. We can't change how it responds to what does. Defence in depth means the application consuming the model output also needs to not blindly trust it.
Novel carriers. Attackers will find hiding places we haven't enumerated. Pattern-based detection is inherently a race, which is why it's one layer of several rather than the whole answer.
Why bother if it's incomplete
Because the alternative is nothing, and the current base rate of these attacks is dominated by unsophisticated variants. Most indirect injection in the wild today is a plain instruction in an HTML comment. Catching the easy 80% is worth doing while the harder cases get better tooling.
It also matters that this runs client-side, before the prompt is sent. Detecting an injection attempt after the model has already acted on it is forensics, not prevention.
If you want to see exactly which patterns we match, the detector is a single readable file in the detection core. We'd rather you audit it than take our word for the coverage.