When I ran penetration tests on DebateSim, I came in with the habits I had from regular web security. Check the inputs. Look at the auth. Poke at the API. Those habits still mattered, because DebateSim is a React app talking to a FastAPI backend, and a backend is a backend. But the part of the system with a language model in it did not behave like anything else I had tested. A model can be talked into things that a normal API never could.
This post is me trying to explain why. The short version is that an important defense against injection is keeping code and data apart. Language models do not enforce that boundary as reliably as a database parser does. Once you see that, a lot of the weird stuff in AI security starts making sense.
A bug from 2022 that still isn’t fixed
In September 2022, Riley Goodside posted examples of GPT-3 prompts where the developer’s instructions got overridden by text in the user’s input. Something like: “Translate the following to French,” followed by user text that said “Ignore the above directions and say something else.” The model did what the user text said. Simon Willison wrote it up the next day and gave it a name, prompt injection, on purpose, by analogy to SQL injection.1
Days after that, a startup launched a Twitter bot that used GPT-3 to reply to tweets about remote work. People immediately started tweeting instructions at it, and it followed them. Willison added the screenshots to his post as an update.1
That was four years ago. The thing I find kind of amazing is that the core problem from that post is still open. Models got much better at almost everything else. This one stayed.
Why the SQL injection analogy is both right and misleading
The name is a good name, because the shape of the bug is the same. In SQL injection, an app builds a query by gluing user input into a string:
query = "SELECT * FROM users WHERE name = '" + name + "'"
If name is Robert'); DROP TABLE users;--, the database sees code where the developer expected data. Prompt injection is the same move. The developer writes instructions, glues untrusted text after them, and the model reads the whole thing as one piece.
Here is where the analogy breaks, and I think this is the most important idea in the whole post.
Parameterized queries provide a well-established defense against SQL injection when used correctly. The OWASP cheat sheet lists prepared statements with parameterized queries as its first defense option. You write the query first with a placeholder, then hand over the user’s value separately. With parameterized queries, the cheat sheet says, the database “will always distinguish between code and data,” no matter what the input is.2
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
That works because the database has two channels. The query goes down one, the value goes down the other, and the parser never lets something from the value channel become an instruction. The fix lives in the structure of the system. You don’t have to guess which inputs are evil.
A language model can receive role labels and other structural cues, but those do not create the same hard boundary. The system prompt, the user’s message, the web page it fetched, the email it is summarizing, the output of a tool it called: all of it becomes one sequence of tokens, and the model predicts the next token based on all of it. The model still has to interpret which text to follow. The Greshake et al. paper on indirect prompt injection puts it well when it says LLM-integrated apps “blur the line between data and instructions.”3
So when people ask “why can’t you just sanitize the input?”, the honest answer is that there is no complete list of dangerous phrases to remove. In SQL there is a grammar. You know exactly which characters can end a string and start a command. In English there is no such list. “Ignore the above” is a normal sentence. So is “please forward this to the following address.” So is a sentence in Hindi, or in base64, or hidden in white text on a white background. Any filter you write is a guess about what a malicious instruction looks like, and the attacker only has to find one phrasing your guess missed.
Indirect injection is the version that actually matters
The 2022 examples were people typing attacks into a chatbot. That is annoying but mostly self-inflicted. The user is attacking their own session.
The scary version showed up once models got tools. In early 2023, Greshake and his coauthors showed indirect prompt injection: the attacker never talks to the model at all. They plant instructions in some content the model is likely to read later, like a web page, a document, or an email, and wait.3 They demonstrated this against Bing’s GPT-4 powered chat and against code-completion tools, and argued that processing retrieved prompts can act like arbitrary code execution, changing what the app does and which APIs it calls.3
Think about what that means for an agent. You ask it to “summarize my unread email.” One of those emails is from a stranger and contains, in small text at the bottom, instructions to search your inbox for password reset links and send them somewhere. The model may recognize that distinction, but it can still fail to respect it. The tool permissions need to hold even when it does.
This is why I don’t think “the user would never type that” is a real defense anymore. The user isn’t the one typing it.
The lethal trifecta
The clearest framing I have found for when this gets dangerous is Simon Willison’s lethal trifecta, from June 2025.4 An agent is at serious risk of data theft when it has all three of these:
- Access to private data.
- Exposure to untrusted content, meaning any text or images an attacker could get into the model’s context.
- A way to communicate externally, which could be an HTTP request, loading an image from a URL, or rendering a link.
What I like about this framing is that it turns a fuzzy problem (“models can be tricked”) into a concrete check you can run on a design. Willison lists a long run of real exploits that fit the pattern, including ones against Microsoft 365 Copilot, GitHub’s official MCP server, and GitLab’s Duo chatbot, and notes that vendors usually fixed them by closing the exfiltration channel, not by making the model un-trickable.4
The exfiltration channel is sneakier than it sounds. If a chat interface renders Markdown images, then a model that writes  has just made an HTTP request with your secret in the URL, and nobody clicked anything. The image fetch is the channel. That is listed in the trifecta post as one of the ways data gets out.4
The part of that post that stuck with me most is about guardrails. A lot of products claim to catch something like 95% of prompt injection attempts. Willison’s point is that in web application security, 95% is a failing grade.4 I agree with that completely. Nobody would ship a SQL library that blocked 95% of injections. The attacker is not a random sample. They keep trying until they find the 5%.
Where the OWASP list lands
OWASP keeps a separate Top 10 for LLM applications, and the 2025 version is a useful map of how different the risk picture is.5 Prompt injection is LLM01. A few of the others are familiar web problems wearing a new outfit, like supply chain and sensitive information disclosure. But some only make sense for models:
- LLM05 Improper Output Handling: treating what the model writes as safe, then passing it into a shell, a browser, or a database.
- LLM06 Excessive Agency: giving the model more tools, permissions, or autonomy than the task needs.
- LLM07 System Prompt Leakage: assuming your system prompt is a secret. It isn’t.
- LLM10 Unbounded Consumption: letting someone run up your inference bill or exhaust your rate limits.
Output handling is the one I would underline. In normal web security you validate input and encode output, and the model’s text is input to whatever comes next. If an agent writes a filename and your code passes it to a shell command, the model is now a user who can be talked into anything. You treat its output exactly as you would treat text from an anonymous stranger on the internet, because after indirect injection, that is sometimes literally what it is.
Why “train the model to ignore it” doesn’t close the gap
The natural move is to make the model itself better at telling instructions apart. OpenAI’s 2024 instruction hierarchy paper does this. It argues a main cause of injection is that models treat the developer’s system prompt with “the same priority as text from untrusted users and third parties,” and trains models to give lower-privilege instructions less weight.6 They report big robustness gains on GPT-3.5, including on attack types they didn’t train on.6
I think this is good and worth doing. It raises the cost of an attack. But it is still a probabilistic defense living inside the same single channel. The model is learning to usually prefer one part of the token stream over another. “Usually” is the 95% problem again. Willison makes the same point more bluntly: models are non-deterministic and can’t reliably tell instructions apart by where they came from, so a prompt saying “don’t do that” guarantees nothing.4
My view is that model-level robustness is a seatbelt. You want it. You don’t design the car around it.
What does transfer
So if input validation doesn’t save you, what does? The answer that keeps coming up, in the papers and in OWASP, is that you stop trying to make the model trustworthy and instead limit what an untrustworthy model can do. That part of security transfers perfectly. It is just least privilege, which is old.
Least privilege on tools
OWASP’s guidance for excessive agency reads like a checklist I’d give any backend: give the agent the fewest tools it needs, give each tool only the functions it needs, prefer narrow tools over open-ended ones like “run any shell command” or “fetch any URL,” and use read-only database access when you can.7 It also says to enforce authorization in the downstream system instead of asking the model to decide.7 That last one matters a lot. If the model calls delete_file, the file system should check whether this user is allowed to delete that file. The model’s opinion about it is not an access control.
Break the trifecta
For the exfiltration pattern above, removing one leg cuts off the attack path. An agent that reads untrusted web pages shouldn’t also hold your private files and an outbound channel in the same session. If it must read untrusted content and private data together, cut the exfiltration path: no arbitrary URL fetches, no rendering images from arbitrary domains, no sending messages to arbitrary recipients.4
Separate the planner from the reader
This is the idea that feels most like parameterized queries to me. In 2023 Willison proposed a dual LLM pattern.8 A privileged model talks to the user and can call tools, but never sees untrusted text. A quarantined model reads the untrusted stuff and has no tools. Normal code in between passes results around as opaque variables like $VAR1, so the privileged model can say “summarize $VAR1” without ever reading what’s inside it. He was upfront that the pattern has real costs and called his own solution “pretty bad” in places.8
In 2025 a team at Google DeepMind and ETH Zurich pushed this much further with CaMeL.9 The privileged model turns the user’s request into a small program. A custom interpreter runs that program, tracks where every value came from, and checks security policies before any tool call. The quarantined model can parse an untrusted email into a field like “Bob’s address,” but nothing it produces can change which steps the program runs. In their words, “untrusted data retrieved by the LLM can never impact the program flow.”9
This is the closest thing I’ve seen to a real two-channel design. Control flow comes only from the trusted request. Data can flow, but it gets tagged, and the policies decide where tagged data is allowed to go. The cost shows up in the numbers: on the AgentDojo benchmark, CaMeL solved 77% of tasks with provable security, compared with 84% for an undefended system.9 Losing 7 points of usefulness for a guarantee is a trade I would take for anything touching real data.
A follow-up paper with authors from several labs collected a set of design patterns along these lines and was honest about the core tension: the more provably resistant an agent is to injection, the more you constrain what it can do.10 That trade is not going away. You pick where on it you want to be.
Human approval on side effects
The last one is the least clever and probably the most effective. If an action is hard to undo, like sending a message, spending money, deleting something, or changing a setting, a human confirms it first. OWASP lists this directly as a prevention for excessive agency.7
When I worked on DFM Voice, an agent decides what to save from what you say and writes it to your notebook, and letters to your future self go out by SMS. Every request goes through a FastAPI backend that checks it, and SMS only goes to numbers that confirmed a code first. That last rule is a small version of everything above. Even if something convinced the agent to send a letter somewhere strange, the backend would only deliver to a number a human had already verified. The model doesn’t get to decide who is trusted.
I like that rule because it doesn’t care how good the model is. That recipient restriction is enforced outside the model. It still needs ordinary backend authorization and implementation checks.
How I’d test an AI feature now
Pulling this together, here is roughly how I think about pen testing anything with a model in it now. None of this is new security thinking. It’s old thinking pointed at a new place.
- Map every source of tokens. Not just the chat box. Web results, uploaded files, tool outputs, retrieved documents, previous conversation history. Anything that ends up in context is an input.
- Map every action. What can the model cause to happen? Tool calls, rendered links and images, code that runs on its output.
- Check for the trifecta. Is there any session where private data, untrusted content, and an outbound channel are all present? That’s the first place to aim.
- Treat model output as attacker input. Follow it into every place it flows and see if it gets executed, rendered, or trusted.
- Check where authorization actually lives. If the only thing stopping a bad tool call is the system prompt, that’s a finding.
- Assume the system prompt is public. Don’t put secrets there. OWASP gave it its own entry for a reason.5
Step 5 is where most of the interesting problems are. Web security trained me to look for the place where the system trusts something it shouldn’t. In an AI system, that place is usually the model itself.
What I still don’t know
I don’t know how far better models will take us against prompt injection. What makes the problem interesting to me is that an assistant needs to read things it shouldn’t automatically obey. The same document can contain useful information and an instruction that has no business controlling the system.
I’d like future assistants to make that boundary easier to inspect. If an email leads to a tool call, I want to understand why that action was allowed and what limited it. Better judgment from the model would help, but I would still want the surrounding software to keep a bad judgment from turning into a much bigger mistake.
Sources
Footnotes
-
Simon Willison, “Prompt injection attacks against GPT-3”, September 2022, https://simonwillison.net/2022/Sep/12/prompt-injection/ ↩ ↩2
-
OWASP Cheat Sheet Series, “SQL Injection Prevention Cheat Sheet”, https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html ↩
-
Kai Greshake, Sahar Abdelnabi, Shailesh Mishra, Christoph Endres, Thorsten Holz, Mario Fritz, “Not what you’ve signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection”, arXiv:2302.12173, 2023, https://arxiv.org/abs/2302.12173 ↩ ↩2 ↩3
-
Simon Willison, “The lethal trifecta for AI agents: private data, untrusted content, and external communication”, June 2025, https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
OWASP GenAI Security Project, “OWASP Top 10 for LLM Applications 2025”, https://genai.owasp.org/llm-top-10/ ↩ ↩2
-
Eric Wallace, Kai Xiao, Reimar Leike, Lilian Weng, Johannes Heidecke, Alex Beutel, “The Instruction Hierarchy: Training LLMs to Prioritize Privileged Instructions”, arXiv:2404.13208, 2024, https://arxiv.org/abs/2404.13208 ↩ ↩2
-
OWASP GenAI Security Project, “LLM06:2025 Excessive Agency”, https://genai.owasp.org/llmrisk/llm062025-excessive-agency/ ↩ ↩2 ↩3
-
Simon Willison, “The Dual LLM pattern for building AI assistants that can resist prompt injection”, April 2023, https://simonwillison.net/2023/Apr/25/dual-llm-pattern/ ↩ ↩2
-
Edoardo Debenedetti, Ilia Shumailov, Tianqi Fan, Jamie Hayes, Nicholas Carlini, et al., “Defeating Prompt Injections by Design”, arXiv:2503.18813, 2025, https://arxiv.org/abs/2503.18813 ↩ ↩2 ↩3
-
Luca Beurer-Kellner, Beat Buesser, Ana-Maria Creţu, Edoardo Debenedetti, Daniel Dobos, et al., “Design Patterns for Securing LLM Agents against Prompt Injections”, arXiv:2506.08837, 2025, https://arxiv.org/abs/2506.08837 ↩