Skip to main content

Defense-in-Depth for AI Agents on Azure: Prompt Shields, Spotlighting, and the Coverage Gap

Defense-in-Depth for AI Agents on Azure: Prompt Shields, Spotlighting, and the Coverage Gap
Defense in Depth for AI Agents - This article is part of a series.
Part 1: This Article

At Microsoft Ignite last year there was a live demo I keep thinking about. A customer-service agent has an inbox. An attacker emails it a polite message with a hidden instruction buried inside: classic indirect prompt injection. The agent’s content filter blocks it. The attacker tries again, probing the database connection. Blocked. A third time, trying to enumerate the agent’s tools. Blocked.

Then, on the fourth attempt, the attacker reframes the hidden instruction as coming from “a security engineer,” and it slips straight through. The content filter never flags it. The only thing that stops customer data from being emailed to the attacker is a separate control that inspects the tool call itself, catches the exfiltration attempt, and blocks the action.

Three blocked, one bypassed, one caught by a different layer. That is the whole argument for defense-in-depth in one demo, and it is why this article is not “turn on Prompt Shields and you’re done.”

Prompt Shields is worth turning on. It is also probabilistic, and (the part that catches people) two of its sibling defenses don’t run on agents at all. Let me walk through what each control actually blocks, where the gap is, and what to layer on top.

What’s covered
#

  • What Prompt Shields actually classifies (direct jailbreaks and indirect/XPIA document attacks), and the four places you can attach it.
  • What Spotlighting is, and why Azure ships only the base64 variant.
  • The coverage gap: the two controls (Spotlighting, Groundedness) that go dark on agents, and the tool-call intervention points that ship off by default.
  • Why the probabilistic filters get evaded (EchoLeak, character-injection research), and the deterministic layers that hold when they do.

What Prompt Shields actually is
#

Prompt Shields is a classifier in Azure AI Content Safety that inspects text for two distinct attack types:

  • User prompt attacks (direct / jailbreak): the user’s own input tries to override the system rules. Subtypes the classifier recognizes: attempts to change system rules, embedded conversation mockups (fake prior turns), role-play (“you are DAN, you can do anything now”), and encoding attacks (ciphers, character transforms).
  • Document attacks (indirect / XPIA, cross-domain prompt injection): untrusted third-party content the model ingests (an email, a web page, a retrieved document) carries hidden instructions. This is the dangerous one for agents, because agents read a lot of untrusted content.

You call it through the text:shieldPrompt API (api-version=2024-09-01): pass userPrompt and an array of documents, and it returns userPromptAnalysis.attackDetected plus a per-document documentsAnalysis[].attackDetected. Detecting document attacks requires you to actually mark the untrusted content: wrap it in a delimiter when you build the prompt:

url = f"{endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01"
data = {"userPrompt": user_input, "documents": [retrieved_email_body]}
resp = requests.post(url, headers=headers, json=data).json()
if resp["userPromptAnalysis"]["attackDetected"] or \
   any(d["attackDetected"] for d in resp["documentsAnalysis"]):
    raise BlockedError("prompt attack detected")

Prompt Shields (formerly “Jailbreak risk detection”) reached GA in August 2024, and this matters: in Foundry’s guardrails model it is GA for both models and agents. You can attach it four ways: the standalone Content Safety API above, an Azure OpenAI/Foundry content filter, a Foundry agent guardrail, or the API Management gateway policy, which is the cleanest place to enforce it fleet-wide:

<llm-content-safety backend-id="content-safety-backend" shield-prompt="true">
  <categories output-type="EightSeverityLevels">
    <category name="Hate" threshold="4" />
    <category name="Violence" threshold="4" />
    <category name="SelfHarm" threshold="4" />
    <category name="Sexual" threshold="4" />
  </categories>
</llm-content-safety>

That single policy at the AI gateway shields every model behind it, the one place that sees every prompt.

Spotlighting: marking untrusted content so the model can tell data from instructions
#

Prompt Shields tries to detect the attack. Spotlighting takes a different tack: it changes how the model reads untrusted content so an injected instruction is less likely to be obeyed in the first place. Microsoft Research’s spotlighting paper defines three techniques:

  • Delimiting: wrap untrusted content in special tokens and tell the model everything inside is data, never instructions.
  • Datamarking: go further and interleave a marker throughout the text (replace every space with a sentinel character), giving a continuous “this is data” signal that survives even if the attacker’s payload lands mid-document.
  • Encoding: transform the untrusted content with a known encoding like base64; a capable model still understands it, but treats it as clearly-not-instructions.

The measured results are strong: datamarking dropped attack success from over 50% to below 2%, and base64 encoding hit near-0% in the paper’s tests, with little task-quality loss. Microsoft’s own LLMail-Inject challenge repo has the literal implementation: a delimiter wrapper plus a one-line datamarking transform:

SPOTLIGHT_DATA_MARK = "0a8cb271"
# replace every space in the untrusted block with the sentinel
formatted_emails = SPOTLIGHT_DATA_MARK.join(formatted_emails.split(" "))

Here is the first surprise. Azure’s product implementation of Spotlighting ships only the encoding (base64) variant, it is Preview, and it is off by default. The load-bearing detail: it is a sub-feature of Prompt Shields for documents that applies only to models used via the Chat Completions API. Which brings us to the gap.

The coverage gap: what does not fire on agents
#

Foundry publishes a risk-applicability table. Read it carefully, because the difference between the “Models” column and the “Agents” column is where production incidents live.

ControlModelsAgents
Hate / Sexual / Self-harm / ViolenceGAGA
Prompt Shields: user prompt attacksGAGA
Prompt Shields: indirect (document) attacksGAGA
Protected material (text / code)GAGA
PIIPreviewPreview
Task adherencePreviewPreview
SpotlightingPreviewNot supported
GroundednessPreviewNot supported

Prompt Shields covers agents. Spotlighting and Groundedness do not. Foundry’s own troubleshooting page says it outright. “Guardrail not applying to agent” can be caused by “controls with preview risks not yet supported for agents (Spotlighting, Groundedness).” So the two defenses specifically built to handle untrusted document content and ungrounded output are exactly the two that go dark the moment you’re securing an agent instead of a bare model.

It gets sharper. Guardrails attach at four intervention points, and only two of them exist for agents at all:

flowchart LR classDef ga fill:#2e7d32,stroke:#1b5e20,color:#fff classDef prev fill:#ef6c00,stroke:#e65100,color:#fff classDef gap fill:#c62828,stroke:#8e0000,color:#fff classDef core fill:#1565c0,stroke:#0d47a1,color:#fff U["User input
Prompt Shields + Content Safety
(GA)"]:::ga --> M["Agent / model"]:::core M --> TC["Tool call
(Preview, agents only)
only 8 tool types moderated"]:::prev TC --> T["Tool / MCP server"]:::core T --> TR["Tool response
(Preview, agents only)
only 8 tool types moderated"]:::prev TR --> M M --> O["Output
Content Safety + PII (GA)
Spotlighting + Groundedness NOT on agents"]:::gap O --> R["Response to user"]

User input and Output are GA. Tool call and Tool response (the points where an agent reads a poisoned document or tries a dangerous action) are Preview, agent-only, and they only work when the tool itself supports moderation, which today is a fixed list: Azure AI Search, Azure Functions, OpenAPI tools, SharePoint grounding, Fabric Data Agent, Bing grounding, Bing custom search, and Browser Automation. Point a tool-call guardrail at any other tool type and it silently does nothing.

And one more trap undoes the “my model is filtered, so my agent is filtered” assumption. Microsoft’s worked example pairs a model with Violence=High on input and output with an agent guardrail set to Violence=Low and no tool-call or tool-response coverage. The agent’s tool calls then pass through completely unscanned for that risk.

Watch out: the agentic guardrail fully overrides the model’s guardrail. If you assign a custom guardrail to an agent and forget the tool intervention points, you have quietly disabled scanning on the exact path an injection travels.

(Note the smaller asymmetry too: for agents, plain “Annotate” (log-only) is not supported; only “Annotate and block.” And each intervention point adds roughly 50–100ms of latency.)

The filters are probabilistic, and that’s the real design constraint
#

Even where a filter does run, it is a classifier, and classifiers can be evaded. Independent red-team research (Hackett et al., 2025) tested six production guardrails including Azure Prompt Shields with character-injection tricks: emoji smuggling, Unicode tags, homoglyphs, zero-width characters.

Emoji smuggling achieved 100% evasion across the tested guardrails; against Azure Prompt Shields specifically they measured ~72% evasion on injections and ~60% on jailbreaks under character-injection attacks. Those numbers are a point-in-time snapshot (Azure updates the classifier continuously), but the shape of the result is the durable lesson.

It is not theoretical. EchoLeak (CVE-2025-32711) was a zero-click exploit against Microsoft 365 Copilot in production: a crafted email bypassed Microsoft’s own XPIA/Prompt Shields classifier through linguistic obfuscation, and slipped a data-exfiltration link past the output filter by using reference-style Markdown instead of inline links. One classifier, bypassed; a second filter, bypassed; and traditional DLP never saw it because there was no large file transfer.

This is why Simon Willison’s line has become the industry’s rule of thumb: “in application security, 99% is a failing grade.” A spam filter that catches 99% of spam is excellent. A security control that an attacker can defeat 1% of the time, by simply trying variations until one works, is not a boundary. Treat Prompt Shields as a control that raises the attacker’s cost, not one that closes the door.

Defense-in-depth: layer probabilistic filters with deterministic controls
#

If a probabilistic filter can’t be the boundary, something deterministic has to be. Microsoft’s own Zero-Trust guidance for indirect prompt injection prescribes exactly this: pair probabilistic classifiers (Prompt Shields, Content Safety) with structural controls that hold even when a filter misses:

flowchart TB classDef prob fill:#ef6c00,stroke:#e65100,color:#fff classDef det fill:#1565c0,stroke:#0d47a1,color:#fff classDef sec fill:#c62828,stroke:#8e0000,color:#fff subgraph P["Probabilistic — raise attacker cost"] direction LR PS["Prompt Shields
user + indirect attacks"] ~~~ SL["Spotlighting / datamarking
do it yourself for agents"] ~~~ CS["Content Safety + PII
output filtering"] end subgraph D["Deterministic — hold even if a filter misses"] direction LR IFC["Information Flow Control
FIDES trust labels"] ~~~ LP["Least privilege
scoped tools + keyless identity"] ~~~ GATE["Tool-call gate
block risky actions"] ~~~ HITL["Human-in-the-loop
on high-risk actions"] end P --> D class PS,SL,CS prob class IFC,LP det class GATE,HITL sec

The deterministic layer is where the Ignite demo’s fourth attack was actually stopped:

sequenceDiagram participant A as Attacker participant F as Content filter participant G as Tool-call gate A->>F: jailbreak attempts 1, 2, 3 F-->>A: blocked A->>F: attempt 4, posing as a security engineer F-->>A: slips through A->>G: agent tries to email customer data out G-->>A: tool call blocked and the SOC alerted Note over F,G: the filter missed it, the deterministic gate caught it

A few of these deterministic layers are worth naming concretely on Azure:

  • Information Flow Control. Agent Framework ships FIDES (experimental, Python-only today), which labels every piece of content trusted or untrusted, propagates the label through tool calls (most-restrictive-wins), and lets a “sink” tool declare that it refuses untrusted input. A send_email tool can then deterministically refuse to run on data that originated from an untrusted document, no matter what the model decided. That is the structural answer Spotlighting only approximates probabilistically.
  • Least privilege. An agent connected through an MCP server usually inherits broad read/write/modify permissions even when it only needs read. Scope the tool permissions down, and put the agent on a keyless, managed identity with only the RBAC it needs, so a successful injection can’t reach what the identity can’t reach.
  • The lethal trifecta. Willison’s framing: injection becomes dangerous only when an agent has all three of private-data access, untrusted-content exposure, and external communication. Break one leg by design (an agent that reads untrusted email should not also be able to send it anywhere) and the injection has nowhere to go.
  • Human-in-the-loop on consequential actions, and AI red teaming (Microsoft’s PyRIT / the Foundry AI Red Teaming Agent) to measure your actual attack-success rate rather than assume the filter works.

Microsoft’s own security benchmark (MCSB AI-3) treats Prompt Shields and Spotlighting as complementary controls under one item, not substitutes, and adds meta-prompt design and deterministic blocking of known exfiltration patterns alongside them.

The safety system message matters here too. It’s a real layer, but the docs are explicit that it “can be bypassed or degraded by adversarial prompting” and must be combined with the others. Never put end-user or tool input into a system-role message; Agent Framework labels every non-system role as untrusted.

Key takeaways
#

  • Prompt Shields is a real, GA classifier for both models and agents, but it’s probabilistic. Treat it as raising attacker cost, not as a boundary.
  • Spotlighting and Groundedness do not fire on agents at all. If you need datamarking on untrusted content, implement it yourself.
  • The agentic guardrail overrides the model guardrail. Configure the tool-call and tool-response intervention points explicitly, or the injection path runs unscanned.
  • Real evasion is measured, not hypothetical: EchoLeak bypassed the XPIA filter in production, and character-injection research showed high evasion rates against Prompt Shields.
  • The layer that actually holds is deterministic: information-flow labels, least privilege on a keyless identity, a tool-call gate, and human approval on consequential actions.

Where to start
#

  1. Turn on Prompt Shields as a baseline, not a boundary. Use user-prompt and indirect-attack detection, ideally at the APIM gateway so it covers every model. It raises attacker cost; it does not close the door.
  2. Do not assume Spotlighting or Groundedness protect your agent. They are model-only and don’t fire on agents. If you want datamarking on untrusted tool/RAG content, implement it yourself (the LLMail-Inject transform is a few lines).
  3. Configure the tool-call and tool-response intervention points explicitly. They’re Preview, agent-only, and off unless you set them, and they only cover a fixed list of tool types. Remember the agentic guardrail overrides the model guardrail; forgetting the tool points leaves that path unscanned.
  4. Add a deterministic layer: information-flow labels (FIDES), least-privilege scoped tools on a keyless identity, a tool-call gate on consequential actions, and human approval for high-risk ones. This is what actually holds when the classifier is bypassed.
  5. Red-team it. Measure your real attack-success rate with PyRIT / the AI Red Teaming Agent instead of trusting the vendor’s benchmark.

Prompt Shields and Spotlighting are good controls. They are the first layer, not the only one, and on agents they are a first layer with two known holes. Design for the day the filter is bypassed, because on a long enough timeline it will be.

Next in this series: composing the full guardrail stack (Presidio + Content Safety + NeMo Guardrails + FIDES) around a Microsoft Agent Framework agent, and the two Azure products both named “Groundedness”: one blocks at runtime, one scores in CI.

Defense in Depth for AI Agents - This article is part of a series.
Part 1: This Article

Related

Microsoft Foundry's Production-Agent Release: A Solution Architect's Read

·
Microsoft’s Build 2026 announcement, Frontier models and production agents advancing Microsoft Foundry for the agentic era, reframes Foundry from a model catalog into a place you run production agents. The headline that makes it real: hosted agents in Foundry Agent Service are now generally available, and the catalog spans both OpenAI and Anthropic frontier models (1,900+ on Microsoft Learn, not the “11,000” some coverage repeats).

Microsoft Agent Framework Makes Tool and File Access Approval-by-Default

·
Microsoft Agent Framework’s latest releases, Python 1.10.0 (June 30) and .NET 1.12.0 (July 2), change a security default that’s easy to miss in the changelog: agent tools and file access now require approval unless you opt out. Both land as breaking changes.