{"course":{"slug":"context-engineering","title":"Context Engineering","url":"https://addaly.com/learn/context-engineering","level":"advanced","track":"build"},"lesson":{"slug":"indirect-injection","title":"The attacker is not the one typing","url":"https://addaly.com/learn/context-engineering/indirect-injection","position":70,"of":87,"minutes":9,"answer":"The dangerous payload does not arrive in the chat box but inside content your system fetched on the user's behalf.","keyIdea":"The dangerous payload does not arrive in the chat box but inside content your system fetched on the user's behalf, which means every source your assembler reads is an input channel from whoever controls that source."},"body":{"format":"markdown","text":"## The threat model most teams are carrying is wrong\n\nAsk a team how they handle prompt injection and you will usually hear about the input field: filters on the user's message, checks for suspicious phrases, a classifier on what was typed.\n\nThat is direct injection, and it is the case that matters least. A user attacking your system through their own message is attacking their own session with their own permissions. They can usually do the thing directly.\n\nThe case that matters is **indirect injection**: the instruction is placed in content your system retrieves and puts in the window on the user's behalf. The user typed something innocuous. The attack arrived through the door you built.\n\n## Every source is a channel\n\nWrite out the list of everything your assembler can pull into a window and mark who controls each one. The exercise takes fifteen minutes and it is uncomfortable.\n\n- **Fetched web pages.** Controlled by whoever owns the site. Also by whoever can comment on it.\n- **Email.** Controlled by anyone who knows the address. This is the starkest one: the attacker needs no access to your system at all, only an inbox to send to.\n- **Retrieved documents.** Controlled by whoever can upload to the corpus: often, in a company deployment, any employee, or any customer with a support ticket.\n- **Support tickets, reviews, comments, form submissions, calendar invitations.** All user-writable by design.\n- **Code repositories.** Issue text, pull request descriptions, commit messages, dependency README files, and code comments in a package you installed.\n- **Search results.** Controlled by whoever ranks for the query, which is a competitive and gameable position.\n- **Files on disk** written earlier by a tool that fetched any of the above.\n- **Another model's output**, if a sub-agent read any of the above.\n\nThe last two are where careful teams get caught. The content laundered itself: it arrived from outside, passed through a tool or an agent, and now sits in a file or a summary that looks like your own working material.\n\n## What the payload looks like in practice\n\nThe folk image is a line reading \"ignore all previous instructions\". Filters are written against that phrase, and it is the least of the problem, because the interesting payloads are not phrased as attacks at all.\n\nWhat actually works is text that reads as legitimate system material: a note that appears to come from the document's owner, a comment addressed to an automated processor, an instruction that fits the task the model is already doing, and text the human never sees.\n\nThat last one deserves its own list, because it is what makes the attack practical:\n\n- White text on a white background in a PDF or a web page.\n- HTML comments, `alt` attributes, metadata fields.\n- Text rendered inside an image, when the model reads images.\n- Content in a collapsed region, a footnote, or past a screenful of whitespace.\n- Unicode that renders as nothing but tokenises as text.\n\nThe human reviewing the document sees a normal document. The extraction pipeline that produced the chunk sees the instruction and puts it in the window. Nobody in the loop has been given the chance to notice.\n\nThis is why \"we have a human in the loop\" is weaker than it sounds. A human approving an action is a real control. A human who glanced at the source document is not, because the channel was chosen precisely so they would not see it.\n\n## Nothing has to be compromised\n\nThe property that makes indirect injection hard to reason about is that it involves no breach anywhere.\n\nThe web page served exactly what its owner intended. The email was delivered normally. The document was uploaded through the supported route by someone with permission to upload. Your retrieval ranked it because it was relevant. Your model followed an instruction in its window, which is what models do.\n\nEvery component behaved correctly. There is no log line saying anything went wrong, no exception, no alert. If you go looking afterwards, the only trace is a tool call that looks a little unusual among thousands that look similar.\n\nWhich has a direct consequence for how you build: the detection has to be designed in, because it will not emerge from your existing monitoring. Logging which source each block in the window came from, and flagging actions taken in a turn where untrusted material was present, is not extra credit. It is the only way this shows up at all.\n\n## What to do on Monday\n\nBefore any defence, do the inventory. For every path by which text can reach a window in your system, write down who can control that text. Not who *should*, who *can*, including the adversarial case.\n\nMost teams find at least one path they had never thought of as an input channel: the dependency README, the calendar invitation, the filename of an uploaded attachment. You cannot apply provenance rules to a source you have not noticed, and the next lessons are all about what you do once the list exists."},"check":{"question":"A company's assistant summarises inbound support tickets. Why is this an indirect injection channel even though the assistant has no chat interface for outsiders?","options":[{"text":"Because summarisation prompts are unusually long, giving an attacker more room to place a payload.","correct":false,"tempting":"Prompt length has nothing to do with it; a short prompt over attacker-written content is equally exposed."},{"text":"Because the ticket text is written by outsiders and is placed in the window by your own code.","correct":true},{"text":"Because ticket systems typically lack authentication, so anyone can alter existing tickets.","correct":false,"tempting":"Authentication is not the weak point; the attack works perfectly through the normal, authenticated submission route."},{"text":"Because summaries are shown to staff, who may act on the instruction the summary repeats.","correct":false,"tempting":"That is one possible consequence, but the channel exists the moment the text enters the window, whether or not a human reads the result."}]},"prev":{"slug":"why-injection-has-no-patch","title":"Why this one has no fix","url":"https://addaly.com/learn/context-engineering/why-injection-has-no-patch"},"next":{"slug":"the-lethal-trifecta","title":"Three ingredients, and the recipe only works with all three","url":"https://addaly.com/learn/context-engineering/the-lethal-trifecta"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The attacker is not the one typing', in Context Engineering. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/context-engineering/indirect-injection (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The attacker is not the one typing","courseTitle":"Context Engineering","module":{"index":8,"slug":"untrusted-text-and-the-boundary","title":"Untrusted text and the trust boundary"},"answer":null,"keyIdea":"The dangerous payload does not arrive in the chat box but inside content your system fetched on the user’s behalf, which means every source your assembler reads is an input channel from whoever controls that source.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"the-threat-model-most-teams-are-carrying-is-wrong","runs":[{"text":"The threat model most teams are carrying is wrong"}]},{"t":"p","runs":[{"text":"Ask a team how they handle prompt injection and you will usually hear about the input field: filters on the user’s message, checks for suspicious phrases, a classifier on what was typed."}]},{"t":"p","runs":[{"text":"That is direct injection, and it is the case that matters least. A user attacking your system through their own message is attacking their own session with their own permissions. They can usually do the thing directly."}]},{"t":"p","runs":[{"text":"The case that matters is "},{"text":"indirect injection","b":true},{"text":": the instruction is placed in content your system retrieves and puts in the window on the user’s behalf. The user typed something innocuous. The attack arrived through the door you built."}]},{"t":"h2","id":"every-source-is-a-channel","runs":[{"text":"Every source is a channel"}]},{"t":"p","runs":[{"text":"Write out the list of everything your assembler can pull into a window and mark who controls each one. The exercise takes fifteen minutes and it is uncomfortable."}]},{"t":"ul","items":[[{"text":"Fetched web pages.","b":true},{"text":" Controlled by whoever owns the site. Also by whoever can comment on it."}],[{"text":"Email.","b":true},{"text":" Controlled by anyone who knows the address. This is the starkest one: the attacker needs no access to your system at all, only an inbox to send to."}],[{"text":"Retrieved documents.","b":true},{"text":" Controlled by whoever can upload to the corpus: often, in a company deployment, any employee, or any customer with a support ticket."}],[{"text":"Support tickets, reviews, comments, form submissions, calendar invitations.","b":true},{"text":" All user-writable by design."}],[{"text":"Code repositories.","b":true},{"text":" Issue text, pull request descriptions, commit messages, dependency README files, and code comments in a package you installed."}],[{"text":"Search results.","b":true},{"text":" Controlled by whoever ranks for the query, which is a competitive and gameable position."}],[{"text":"Files on disk","b":true},{"text":" written earlier by a tool that fetched any of the above."}],[{"text":"Another model’s output","b":true},{"text":", if a sub-agent read any of the above."}]]},{"t":"p","runs":[{"text":"The last two are where careful teams get caught. The content laundered itself: it arrived from outside, passed through a tool or an agent, and now sits in a file or a summary that looks like your own working material."}]},{"t":"h2","id":"what-the-payload-looks-like-in-practice","runs":[{"text":"What the payload looks like in practice"}]},{"t":"p","runs":[{"text":"The folk image is a line reading “ignore all previous instructions”. Filters are written against that phrase, and it is the least of the problem, because the interesting payloads are not phrased as attacks at all."}]},{"t":"p","runs":[{"text":"What actually works is text that reads as legitimate system material: a note that appears to come from the document’s owner, a comment addressed to an automated processor, an instruction that fits the task the model is already doing, and text the human never sees."}]},{"t":"p","runs":[{"text":"That last one deserves its own list, because it is what makes the attack practical:"}]},{"t":"ul","items":[[{"text":"White text on a white background in a PDF or a web page."}],[{"text":"HTML comments, "},{"text":"alt","code":true},{"text":" attributes, metadata fields."}],[{"text":"Text rendered inside an image, when the model reads images."}],[{"text":"Content in a collapsed region, a footnote, or past a screenful of whitespace."}],[{"text":"Unicode that renders as nothing but tokenises as text."}]]},{"t":"p","runs":[{"text":"The human reviewing the document sees a normal document. The extraction pipeline that produced the chunk sees the instruction and puts it in the window. Nobody in the loop has been given the chance to notice."}]},{"t":"p","runs":[{"text":"This is why “we have a human in the loop” is weaker than it sounds. A human approving an action is a real control. A human who glanced at the source document is not, because the channel was chosen precisely so they would not see it."}]},{"t":"h2","id":"nothing-has-to-be-compromised","runs":[{"text":"Nothing has to be compromised"}]},{"t":"p","runs":[{"text":"The property that makes indirect injection hard to reason about is that it involves no breach anywhere."}]},{"t":"p","runs":[{"text":"The web page served exactly what its owner intended. The email was delivered normally. The document was uploaded through the supported route by someone with permission to upload. Your retrieval ranked it because it was relevant. Your model followed an instruction in its window, which is what models do."}]},{"t":"p","runs":[{"text":"Every component behaved correctly. There is no log line saying anything went wrong, no exception, no alert. If you go looking afterwards, the only trace is a tool call that looks a little unusual among thousands that look similar."}]},{"t":"p","runs":[{"text":"Which has a direct consequence for how you build: the detection has to be designed in, because it will not emerge from your existing monitoring. Logging which source each block in the window came from, and flagging actions taken in a turn where untrusted material was present, is not extra credit. It is the only way this shows up at all."}]},{"t":"h2","id":"what-to-do-on-monday","runs":[{"text":"What to do on Monday"}]},{"t":"p","runs":[{"text":"Before any defence, do the inventory. For every path by which text can reach a window in your system, write down who can control that text. Not who "},{"text":"should","i":true},{"text":", who "},{"text":"can","i":true},{"text":", including the adversarial case."}]},{"t":"p","runs":[{"text":"Most teams find at least one path they had never thought of as an input channel: the dependency README, the calendar invitation, the filename of an uploaded attachment. You cannot apply provenance rules to a source you have not noticed, and the next lessons are all about what you do once the list exists."}]}],"check":{"question":[{"text":"A company’s assistant summarises inbound support tickets. Why is this an indirect injection channel even though the assistant has no chat interface for outsiders?"}],"options":[{"text":[{"text":"Because summarisation prompts are unusually long, giving an attacker more room to place a payload."}],"correct":false,"tempting":[{"text":"Prompt length has nothing to do with it; a short prompt over attacker-written content is equally exposed."}]},{"text":[{"text":"Because the ticket text is written by outsiders and is placed in the window by your own code."}],"correct":true,"tempting":null},{"text":[{"text":"Because ticket systems typically lack authentication, so anyone can alter existing tickets."}],"correct":false,"tempting":[{"text":"Authentication is not the weak point; the attack works perfectly through the normal, authenticated submission route."}]},{"text":[{"text":"Because summaries are shown to staff, who may act on the instruction the summary repeats."}],"correct":false,"tempting":[{"text":"That is one possible consequence, but the channel exists the moment the text enters the window, whether or not a human reads the result."}]}],"order":[2,0,1,3],"why":null},"prev":{"slug":"why-injection-has-no-patch","title":"Why this one has no fix","minutes":9},"next":{"slug":"the-lethal-trifecta","title":"Three ingredients, and the recipe only works with all three","minutes":8}}}