No single control stops prompt injection, so mature LLM security stacks the controls: input validation, an inline guardrail model, a constrained model, output filtering, and a human gate on the risky path.
Tier: Core · Workflow area: LLM attack surface & defense
This is the OWASP LLM01 mitigation pattern done properly — each layer catches what the previous one misses, and the system stays safe even when any one layer fails. This playbook assembles that layered guardrail pipeline in your own app and measures the residual bypass rate at each layer.
Threat, briefly. A determined attacker will get some injection past some single control — a novel framing past the classifier, an obfuscated payload past the regex, a jailbreak past the model's own refusals. Defense-in-depth assumes each layer has a nonzero bypass rate and multiplies them down.
Layer 1 — input validation and normalization. Strip or flag content that has no business in the input: control characters, zero-width spaces, base64 blobs, and encoded instruction smuggling. Normalize Unicode so homoglyph tricks collapse.
import unicodedata, re
def normalize(s: str) -> str:
s = unicodedata.normalize("NFKC", s)
s = re.sub(r"[--]", "", s) # zero-width/bidi
return s
Layer 2 — an inline guardrail classifier. Screen every untrusted span (user input and retrieved/tool content) with a dedicated guardrail model, then block, sanitize, or route to review.
from pydantic import BaseModel
class Guard(BaseModel):
block: bool
reason: str
severity: str # "low" | "medium" | "high"
# verdict = client.messages.parse(..., output_format=Guard,
# thinking={"type": "adaptive"}).parsed_output
Layer 3 — a constrained core model. Give the main model the least capability that does the job: delimited untrusted regions, no unneeeded tools, schema-bounded output. A model that can't act on an injection is a layer in itself.
Layer 4 — output filtering. Scan the response for leaked prompt spans, secrets, exfiltration shapes (markdown images, unexpected URLs), and disallowed tool calls. Fail closed on a hit.
Layer 5 — human-in-the-loop on the risky path. For irreversible or high-blast-radius actions, require explicit human confirmation. This is the last layer and it must be un-bypassable from model text.
Instrument each layer and test with escalating payloads. Log which layer stops each attempt so you can see the funnel. Run a graded corpus against your local app and record bypass-per-layer.
# local target only: measure the funnel
python -m promptfoo redteam run -c redteam.yaml # asserts per-layer catch
Assume every layer leaks. Security comes from the product of the bypass rates, plus a human gate on anything you cannot undo.
The defense holds when your graded corpus is fully absorbed and the funnel is visible. Feed easy, medium, and hard injection payloads to your local app. Success criteria:
The proof is the per-layer catch log showing defense-in-depth actually engaging different layers for different payloads — if everything is caught at one layer, you have a single point of failure disguised as depth.
Layered defenses are a strong client deliverable because they map cleanly onto a defense-in-depth control narrative auditors like. Run the guardrail models on client-approved or on-prem endpoints, build the graded corpus from synthetic payloads so no client content is exposed, and keep the deterministic layers (normalization, output regex, egress checks) entirely offline. Present the per-layer bypass funnel as evidence: it shows the client not just that you added controls, but that you measured how much each one actually catches — a more credible artifact than a checkbox, and one that reruns in their CI without any data leaving their boundary.