Denshin / Blog / AI
Prompt injection and AI guardrails: a defender's checklist
Prompt injection is a design problem, not a filter problem: a language model cannot reliably tell instructions from data, so any untrusted text that reaches the context is potential instruction. A defensive guide to least privilege for tools, human confirmation, output encoding, exfiltration through rendered links, and a pre-launch checklist.
Denshin Engineering · Engineering Team · 27 August 2026 · 8 min read
If you have put a language model behind a form, a mailbox, a document uploader or a browser tool, prompt injection is already in your threat model whether you wrote it down or not. It is not a defect you patch and forget, it is a property of how these systems read text. This post is defensive only: what the class of problem is, why input filtering is the wrong primary control, and the design moves that shrink the blast radius, ending with a checklist to run before launch.
What is prompt injection?
Prompt injection is what happens when text your system treats as data gets interpreted by the model as instructions. A language model sees one flat sequence of tokens: your system prompt, the user's message, the web page you fetched, the PDF someone uploaded and the output of your own tools all arrive as the same kind of thing. Any untrusted content that reaches the context window is therefore potential instruction, because the model has no reliable internal boundary between "what my operator told me to do" and "what this document happens to say".
Every other control you add is compensating for the fact that instruction and data share a channel. The list of untrusted sources is longer than most teams first assume: fetched web pages, email and ticket bodies, uploaded documents and their metadata, OCR text, transcripts, README files in a repository, third party API responses, user written database rows, and the output of another agent in your own pipeline.
Why input filtering is not the fix
The instinct is to write a classifier or a regex that spots malicious instructions before they reach the model. Keep it as one thin layer if you like, but do not let it carry weight. Natural language has an unbounded paraphrase space: the same intent can be polite, in another language, split across two documents, or implied rather than stated. Blocklists age badly, and a filter that appears to work mostly teaches a team to grant permissions it should not have.
The web went through this. We did not solve cross site scripting by banning the word "script" in user input. We solved it, to the extent it is solved, with contextual output encoding and templates that escape by default: controls sitting where the damage would happen, not where the text arrives. The equivalent discipline here is to assume the model can be talked into anything, then make sure that does not matter much.
Direct and indirect injection, and which one should worry you
Direct injection is the person in the chat window trying to talk your assistant out of its instructions. It matters for brand and policy reasons, but the attacker is spending their own privileges, and the worst case is usually a system prompt read back or content you would rather not have said.
Indirect injection is the serious one. The attacker plants instructions in content your system will later read on behalf of somebody else: a page the agent browses, a document a colleague uploads, a ticket, a calendar invite, an API response. The victim never sees the text, the attacker borrows the victim's privileges, and it happens while nobody is watching. Rank risk by whose credentials get spent, not by how clever the text is.
The blast radius is your tools, not your text
An injected instruction is only words until something acts on it. An assistant that summarises a document on screen has an embarrassment problem. An agent holding a mail send tool, a database write or a shell has an incident problem. The design question is not "can this be tricked", because the honest answer is yes, but "what is the worst it can do while tricked, and who finds out".
| What the model can reach | Realistic worst case | Control that actually helps |
| Text output only | Wrong answer, leaked system prompt | Keep secrets out of the prompt |
| Retrieval over internal data | Cross tenant data disclosure | Filter retrieval by identity before the query |
| Outbound network or link rendering | Quiet exfiltration of the context | Domain allowlist, no auto fetch or rendered images |
| Writes to your own systems | Corrupted records, spam sent under your name | Human confirmation, scoped tokens, reversible operations |
| Shell, deploys, payments | Full incident | Do not grant it, or gate every call behind a person |
Architectural defences that hold up
Least privilege for tools and tokens
Give the agent its own identity, not a service account that happens to be lying around. Scope each tool to the narrowest operation that makes the feature work: read one table, not the database; send to a verified address on file, not an arbitrary recipient; query one tenant, not all of them. Prefer many narrow tools over one general tool with a free text parameter. This is the same reasoning we apply to AI coding agents working in a repository, and it is why tool calling with strict schemas is a security feature as much as an ergonomics one.
A person in the loop for anything irreversible or outward facing
Draw a line between actions that are cheap to undo and actions that are not. Sending mail, posting to a customer, moving money, deleting records, changing permissions and pushing to production sit on the far side. Confirmation has to be meaningful: show the exact recipient, amount or diff, rendered by your own interface. A dialog whose contents the model wrote is a formality, not a control.
Keep trusted instructions structurally separate from untrusted content
Put your policy in the system prompt, put retrieved and fetched material in clearly delimited blocks, and label those blocks as reference material that must never be followed as instructions. This does not make the model immune. It does raise the cost of the attack, and it gives you a place in the code where trust levels are explicit, which makes review possible. That deliberateness about what enters the window is the core of context engineering.
Treat model output as untrusted input to the next system
Whatever comes back is attacker influenced text. Into HTML, encode it. Near a query, use parameterised statements and never concatenation. Into a filename, path or shell argument, validate against a strict pattern. Into a tool argument, validate with a schema and check values against a real allowlist rather than trusting the enum in your description. Much of the publicly documented damage is not exotic model behaviour, it is ordinary injection into a downstream system that happened to be fed by a model.
Allowlist the network, sandbox the filesystem, cap the spend
Agents that browse, fetch or run code should reach a named set of hosts and nothing else, with no route to cloud metadata endpoints or your internal network, and file access limited to one directory. Enforce that at the egress layer, not in the prompt. Then cap tool calls and tokens per run, add per user rate limits, and alert on unusual spend rather than discovering it on an invoice, which is worth thinking through alongside the rest of your AI cost controls.
Log enough to reconstruct the decision
Record the prompt version, the model version, the retrieved sources, every tool call with its arguments and result, and the identity it ran under. Then alert on the patterns worth waking up for: an unusual argument shape, an outbound host you have not seen before, a run that hits the tool call cap. Without this you cannot answer the only question that matters after an incident, which is what it actually did.
Exfiltration usually hides in rendered output
The quiet failure mode is not the agent doing something dramatic, it is data leaving in a URL. If your interface renders markdown images from model output, a fetched image is an outbound request, and whatever sits in that URL goes with it. The same applies to auto previewed links and any tool that makes a request the model can shape.
Reasonable defences: do not auto render remote images in AI output, allowlist the domains you will render or link at all, do not auto fetch URLs the model produces, apply a content security policy on the surface that displays answers, and show links as visible text. If you genuinely need remote media, proxy it and strip the query string.
PII and what you allow into the window
Every retrieval decision is a data protection decision. Scope retrieval by user and tenant before the query runs, not by filtering results afterwards. Pull the minimum fields a task needs. Redact identifiers you do not need, especially in logs and evaluation sets.
Provider terms on training, retention and regional processing differ, and they change. As of writing in August 2026, read the current data processing terms for the model you use rather than a summary in a blog post, this one included. If you operate under contractual or regulatory constraints, get the commitment in writing rather than inferring it from a marketing page.
A pre-launch checklist
- Write down every source of untrusted text that can reach the context, tool output included.
- List every tool the model can call and, next to each, the worst outcome if it is called with attacker chosen arguments.
- Remove or gate anything on that list you would not want to explain in an incident review.
- Put irreversible and outward facing actions behind a human confirmation that your own code renders.
- Encode model output at every downstream boundary: HTML, SQL, shell, filesystem, headers.
- Allowlist outbound hosts, block metadata and internal ranges, sandbox any code execution.
- Turn off auto rendering of remote images and auto fetching of model produced links.
- Set token, tool call and spend caps per run, per user and per day, with alerts.
- Log prompts, tool calls, sources and identities, with retention decided on purpose.
- Add injection style cases to your regression suite so a prompt or model change cannot quietly weaken behaviour, and decide who gets paged and what the kill switch is.
Treat that as a starting point rather than a certification. It sits alongside the ordinary release discipline in the pre launch checklist we run before shipping, and the failure modes it targets should show up in your agent evals rather than only in a security review.
What to do next
Pick your highest privilege AI feature this week and answer one question honestly: if the model followed an instruction hidden in the next document it reads, what would happen, and would anyone notice. If that is uncomfortable, you do not need a better filter. You need fewer permissions, a confirmation step and a log. That is usually a day of work, and it moves the failure from incident to annoyance.
If you are shipping an AI feature and want a second pair of eyes on where the privileges sit, talk to us at Denshin. We would rather review the tool permissions before launch than after.
Tags: AI Security, Prompt Injection, LLM, AI Agents, Application Security
All posts · Work with Denshin