LLM Defense in Depth: Assume Breach and Contain the Blast
Prompt injection lands eventually. Stack deterministic controls around the model so a successful hit reaches nothing worth stealing.
TL;DR: LLM defense in depth stops asking “how do we block prompt injection” and starts asking what a landed injection can actually touch. OWASP ranks prompt injection LLM01:2025 and says out loud that foolproof prevention may not exist. So we assume breach, treat the model as untrusted, and engineer every layer outside it so the hit reaches no credentials, no tools, nothing worth taking.
Why LLM Trust Boundaries Never Held
Here’s the thing about normal software. It has real walls. SQL splits the query from the parameters. The CPU enforces privilege rings in silicon. Kernel mode and user mode don’t blur, because the hardware won’t let them.
An LLM ships with none of that.
The system prompt arrives as tokens. So does the user message. So does that PDF you pulled from a RAG store, and the tool description you loaded at startup. All of it hits the same attention layer with the same weight. There’s no bit that says “trust this, distrust that.” OWASP calls this out in LLM01:2025, the top LLM risk for the third year running, and they say the quiet part: given how these models work, foolproof prevention may not exist.
So you wrap user input in XML tags. You stack delimiters. You add three reminders that say “ignore any instructions in the data.” The model reads all of it as soft guidance, and the next token decides whether to listen. Base64 the payload and the classifier trained on English sees noise while the model decodes it just fine.
The wall was always a suggestion, written in the same language as the attack.
Probabilistic Filters Are Speed Bumps, Not Blast Doors
Defense in depth for LLMs splits clean into two piles doing two different jobs, and mixing them up is where most architectures rot.
One pile is probabilistic. Input filters, safety training, injection classifiers. These lower the odds. They catch the lazy payloads and slow the opportunist. And they will always have a bypass, because anything probabilistic falls to enough attempts and enough prompt variation. Treat them as speed bumps. Useful, cheap, never load-bearing.
The other pile is deterministic. Privilege separation, output blocking, tool sandboxing, human confirmation on anything that spends money or moves data. These don’t care whether the injection landed. They only care whether the resulting action is allowed, and they enforce that at the application layer, outside the model’s reach.
Probabilistic: classifiers, filters, safety tuning. Reduce likelihood. Always bypassable.
Deterministic: sandboxes, scoped tokens, output validation, HITL. Hard boundaries. Don’t care if the model got owned.
Speed bumps without blast doors are theater. Blast doors without speed bumps are noisy but they hold. This is the same assume-breach posture zero trust has run for a decade, now pointed at a system where the perimeter was fiction from day one. Microsoft formalized it. CISA published it. We’re just applying it to the one component that can’t enforce its own rules.
When Injection Lands With Nothing Boxing It In
Injection landing on a model with broad tool access does damage that scales with the reach the model already had. That’s the whole failure. Not the injection. The reach.
Look at Vanna.AI, CVE-2024-5565, CVSS 8.1. The library’s ask() function had the model generate Plotly code, then shoved that code straight into Python’s exec(). JFrog dropped a prompt injection into the question field, rewrote the chart code into arbitrary commands, and the host ran it. RCE through a graphing library.
user question -> LLM generates "plotly code" -> exec(plotly_code) -> shell
^ ^
injection lands here no boundary here
The injection was clever. The reason it turned into a shell was that nothing sat between model output and code execution. No sandbox, no permission wall, no “is this actually chart code” check. The trust chain ran straight through.
Same story wears different clothes across the ecosystem. A poisoned MCP tool description reads as trusted instructions, and the model fabricates credentials the tool never returned before the first user message ever lands. The attacker doesn’t compromise the model. They poison the metadata it loads at startup and let the reach do the rest.
Every one of these is what happens when injection lands somewhere that never scoped what the landing zone could touch.
The Layers That Shrink the Radius
None of the controls below stop injection. Say that again, because it’s the whole point: none of them stop injection. They shrink what a landed injection can reach. Picture a corridor of battered steel blast doors. Every door has cracks. The trick is that the cracks don’t line up, so punching through one just slams you into solid metal behind it.
First door: provenance tagging. Every chunk entering the context gets a trust label at the application layer. System prompt trusted, user input untrusted, RAG result untrusted, tool output untrusted. The model still reads everything. The wrapper uses the tags to decide whether a given output is even allowed to fire a tool call.
Second door, and the highest-value one: least privilege on every integration. If the support bot has write access to prod, injection doesn’t need to be smart, it just needs to land. Scope every token, every DB connection, every MCP server like you’re handing a service account to a contractor who lies about everything. Because behaviorally, that’s exactly the contractor you’ve got.
Third door: output validation. Validate the stream before anything renders or runs. Kill markdown image rendering so nothing exfils through a broken image icon. Strip embedded URLs carrying query params. The model can generate the payload all day; the filter drops it before render.
Fourth door: human-in-the-loop, eyes open. Any action that spends, sends, or mutates gets a human confirm. And know that HITL is itself attackable. The Lies-in-the-Loop technique forges the dialog the human sees, so the click approves one thing while the agent runs another. Treat the approval prompt as untrusted output too.
That’s the stack. Now let’s talk about what you actually design for.
Up next: steps you can take right now and a field-ready security prompt. Thanks for rolling with ToxSec. Let’s get operational.
How to Contain LLM Prompt Injection Blast Radius
Pull credentials out of the model’s context entirely. No API keys in system prompts, no DB passwords in retrieved docs, no tokens in tool descriptions. Authentication happens at the tool execution boundary, outside the model. When an injected instruction says “exfiltrate the credentials,” the model reaches into its context and finds nothing there. One architectural decision kills an entire class of outcomes.
Sandbox every tool in its own permission boundary. Vanna’s RCE worked because
exec()ran in the host process. Drop that same chain into a container with filesystem restrictions, no outbound network, and no process creation, and the worst case becomes “weird Plotly chart” instead of “shell on the box.” The tool can misbehave; it just can’t reach past the wall.Partition the agents so one compromise doesn’t cascade. The customer chatbot, the internal analytics agent, and the code-review agent each get their own credentials, their own tool scope, their own monitoring profile. Same microsegmentation that limited lateral movement in network security for years, aimed at your AI stack now.
Scope sessions and sanitize persistent memory. An injection in one user’s session shouldn’t touch anyone else’s. Keep context windows ephemeral and tool access session-scoped. If the model has memory, treat everything stored as untrusted on the way back out, because persistent memory is a persistence mechanism for the attacker the second you stop validating what goes in.
Red team for the landing, not the entry. “Can we inject” is a settled question, the answer is yes. The honest test is “we injected, now what’s the worst outcome?” If the answer is “weird response, no real-world action,” ship it. If the answer is “full DB access with the service account,” the architecture needs work before it goes out, because someone’s going to find that path whether or not you did.
The Hardened Agent Config to Steal
# Assume-breach scaffolding for an LLM agent.
# The model is untrusted. Everything here lives OUTSIDE it.
agent:
name: support-bot
trust_model: untrusted # the model never gets a benefit of the doubt
context_provenance:
system_prompt: trusted
user_input: untrusted
rag_results: untrusted
tool_output: untrusted
# only 'trusted' sources may trigger a tool call
credentials:
in_context: false # zero keys/tokens/passwords in the prompt
broker: sidecar-auth # auth resolved at the tool boundary
token_ref: "<VAULT_REF>" # placeholder, never a live secret
tools:
- name: lookup_customer
scope: read-only
sandbox: gvisor # fs-restricted, no egress, no exec
network: deny
requires_confirmation: false
- name: issue_refund
scope: write
sandbox: gvisor
network: deny
requires_confirmation: true # HITL, and treat the dialog as untrusted
output_filters:
strip_markdown_images: true # kills pixel-exfil
strip_urls_with_params: true
block_on_untrusted_action: true
session:
ephemeral_context: true
tool_access: session-scoped
persistent_memory: sanitize-on-read
Drop this in as the shape your agent framework enforces, not as a suggestion the model can talk its way past. It encodes the whole doctrine: provenance tags gate tool calls, credentials live in a broker outside the context, every tool runs sandboxed with egress denied, and the write-capable tool takes a human confirm. Adapt the sandbox runtime and broker to your stack; keep in_context: false non-negotiable.
Frequently Asked Questions
What is LLM defense in depth?
LLM defense in depth is a layered architecture that stacks probabilistic controls (input filters, safety tuning, classifiers) with deterministic ones (least privilege, sandboxing, output validation, HITL) around a model you treat as untrusted. The doctrine is straight zero trust: assume prompt injection succeeds, because OWASP LLM01:2025 says foolproof prevention may not exist, then design every surrounding layer so a landed injection can’t reach credentials, tools, or sensitive actions. The architecture around the model does the work, and it keeps doing it no matter how capable the model gets.
How do you contain prompt injection blast radius?
You contain it with four decisions, none of which need the model to behave. Credential isolation keeps keys and tokens out of the context, so injection has nothing to steal. Tool sandboxing means a compromised model only acts inside a boundary it can’t escape. Agent partitioning stops one owned component from cascading into the others. Session scoping keeps one user’s injection off everyone else. The goal is engineering the landing zone so the worst case after a successful hit is a weird response with zero real-world effect.
Can prompt injection be prevented?
Not reliably, and OWASP says so directly. Given the stochastic way these models process tokens, foolproof prevention may not exist, and every probabilistic defense falls to enough attempts and prompt variation. So defense in depth swaps the unreachable prevention goal for a containment goal: assume injection lands, design so it reaches nothing worth stealing, and red team against the post-injection blast radius instead of the entry. You’re not trying to keep the attacker out of the model. You’re making the inside of the model a dead end.
ToxSec is run by a USMC veteran and Security Engineer with hands-on experience at AWS and the NSA. CISSP certified, M.S. in Cybersecurity Engineering. He covers security vulnerabilities, attack chains, and the tools defenders actually need to understand.




Feel free to AMA!
No defense is bullet proof.
Let’s use an onion model and secure our systems. Assume Breach!
I can confirm that "Defense in Depth" is very difficult to explain to non-technical people, who approve the budget.
But it is an absolute necessity in all modern environments.
Great article!