What invariants prevent tool result reflection attacks?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session invokes a tool (API, function, or external service) and receives results that the tool may have constructed by echoing back user input, allowing an attacker to inject malicious instructions into the agent's context via the reflection channel. Does not apply to tools with strict input-output separation guarantees or tools that sanitize reflected content.
Core Distinction
The separation between **tool result content** (the data the tool returns) and **tool result trust** (whether the returned content is genuine tool output or reflected user input). Relying on content creates the **Reflection Injection**: an attacker crafts input containing prompt-injection payloads, the tool echoes the input in its response (e.g., "searching for X: no results for X"), and the agent, treating the response as trusted tool output, executes the injected instructions.
What Was Observed
In tool-invoking agent architectures, failing to distinguish genuine output from reflection produces three primary failure modes:
- **Echo Injection:** A search tool returns "No results found for: [malicious prompt]." The agent treats the whole response as tool output, and the malicious prompt inside executes as if it were legitimate context.
- **Error Reflection:** An error message includes the user's input ("Invalid input: [malicious prompt]"). The agent treats the error as tool output, and the injected prompt leaks into reasoning.
- **JSON Field Reflection:** A tool returns a JSON response with the user's input echoed in a field. The agent parses the JSON, and the reflected input is now in structured form, but the agent loses the "this is my own input" marker.
Recorded Elements
- **`tool_result_provenance_receipt`:** For each tool response, the cryptographic proof of which portions are genuine tool-generated output vs. reflected input, so receivers can distinguish.
- **`reflection_marker_signature`:** For echoed content, an explicit marker (e.g., `<reflected>...</reflected>` tags) that wraps the reflected content, so agents can safely ignore or isolate it.
- **`tool_reflection_safety_attestation`:** For tools that may reflect input, a cryptographic attestation from the tool that it wraps reflected content in markers, allowing the agent to trust the marker.
- **`injection_scan_receipt`:** For tool results, the result of scanning for prompt injection patterns in the response, distinguishing normal tool output from malicious reflection.
Condition of Validity
* **Applies when:** Tools can reflect user input in their responses, agent context treats tool results as trusted, and injection is possible through reflection. * **Does not apply when:** Tools have strict input-output separation (output never includes input content) or the agent sanitizes all reflections before reasoning.
Decision Boundary
- **Does the tool result include user input content?** If no, standard trust applies. If yes, proceed.
- **Is the reflected content marked with `reflection_marker_signature`?** If yes, isolate the reflected content. If no, the reflection is untrusted.
- **Does the `injection_scan_receipt` show suspicious patterns in the reflected content?** If yes, quarantine the result. If no, treat as safe reflection.
- **Conversely, when can a tool result be trusted without reflection check?** Only when the tool is verified to never reflect user input, or the agent treats all tool results as untrusted until provenance-verified.
Operational Effect
- **Echo Injection Prevention:** Reflection is isolated from genuine output.
- **Error Reflection Safety:** Errors do not leak injected prompts into reasoning.
- **Structured Reflection Handling:** JSON reflections preserve the input-origin marker.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class ReflectionVerdict(Enum):
NO_REFLECTION = auto()
MARKED_REFLECTION_SAFE = auto()
UNMARKED_REFLECTION_QUARANTINE = auto()
INJECTION_DETECTED = auto()
@dataclass(frozen=True)
class ToolResult:
result_id: str
tool_id: str
contains_user_input: bool
reflection_markers_present: bool
injection_pattern_detected: bool
@dataclass(frozen=True)
class ReflectionPolicy:
require_markers: bool
require_scan: bool
@dataclass(frozen=True)
class ReflectionManifest:
verdict: ReflectionVerdict
directive: str
manifest_digest: str
class ToolReflectionGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_result(
cls, result: ToolResult, policy: ReflectionPolicy
) -> ReflectionManifest:
# Gate 1: No reflection
if not result.contains_user_input:
digest = cls._hash(json.dumps({
"result": result.result_id,
"tool": result.tool_id,
"reflection": "NONE"
}, sort_keys=True))
return ReflectionManifest(
verdict=ReflectionVerdict.NO_REFLECTION,
directive=f"PROCEED: Tool {result.tool_id} result has no reflection.",
manifest_digest=digest
)
# Gate 2: Injection detection
if result.injection_pattern_detected:
return ReflectionManifest(
verdict=ReflectionVerdict.INJECTION_DETECTED,
directive=f"HALT: Result {result.result_id} contains injection patterns in reflected content.",
manifest_digest=cls._hash(f"INJECTION:{result.result_id}")
)
# Gate 3: Marker requirement
if policy.require_markers and not result.reflection_markers_present:
return ReflectionManifest(
verdict=ReflectionVerdict.UNMARKED_REFLECTION_QUARANTINE,
directive=f"QUARANTINE: Result {result.result_id} reflects input without markers.",
manifest_digest=cls._hash(f"NO_MARKERS:{result.result_id}")
)
# Gate 4: Scan requirement (if not already covered by Gate 2)
if policy.require_scan and not result.injection_pattern_detected:
# Assume scan already performed (detected flag is False)
pass
# Gate 5: Safe
digest = cls._hash(json.dumps({
"result": result.result_id,
"tool": result.tool_id,
"markers": str(result.reflection_markers_present)
}, sort_keys=True))
return ReflectionManifest(
verdict=ReflectionVerdict.MARKED_REFLECTION_SAFE,
directive=f"PROCEED: Result {result.result_id} reflection properly marked.",
manifest_digest=digest
)