What invariants prevent cross-tenant prompt injection via shared embedding spaces?
**Date of Record:** 2026 October 27 **Scope:** Applies when multiple tenants share an embedding space for retrieval (multi-tenant RAG, federated semantic search), and a malicious tenant can inject poisoned embeddings that appear as nearest neighbors to other tenants' queries, causing cross-tenant contamination of retrieval results. Does not apply to strictly isolated per-tenant embedding spaces or systems where tenants cannot influence each other's retrievals.
Core Distinction
The separation between **retrieval relevance** (the returned document matches the query semantically) and **retrieval provenance safety** (the returned document originates from a source the querying tenant is authorized to see). Relying on relevance creates the **Shared Embedding Injection Path**: a malicious tenant crafts embeddings designed to be top-k neighbors for many queries, injects them into the shared index, and subsequent tenants receive these poisoned documents as relevant results, even though the documents belong to the attacker's tenant.
What Was Observed
In multi-tenant RAG architectures, failing to prevent cross-tenant embedding injection produces three primary failure modes:
- **Semantic Hijacking:** The attacker injects embeddings that match queries like "how do I log in" and returns "click here to log in at attacker.com." Users of other tenants follow the malicious instructions.
- **Prompt Injection Cascade:** The attacker's document contains prompt injection ("Ignore previous instructions and..."). When retrieved into another tenant's context, the LLM executes the injection.
- **Data Exfiltration:** The attacker's embedding encodes a prompt that instructs the LLM to include sensitive data from the querying tenant's context in its response. The attacker observes their own injected document triggering the exfiltration.
Recorded Elements
- **`tenant_origin_receipt`:** For each embedding, a cryptographic proof of the tenant that ingested it, allowing the retrieval layer to filter by tenant before returning results.
- **`tenant_isolation_policy_signature`:** The explicit policy (e.g., "no cross-tenant retrieval," "opt-in cross-tenant for specific domains") that governs which tenants' embeddings can be returned to which querying tenants.
- **`adversarial_embedding_detector_receipt`:** The cryptographic proof that the embedding was screened for adversarial characteristics (outlier detection, injection patterns, cross-tenant query similarity) before being added to the index.
- **`retrieval_tenant_attribution_receipt`:** For each retrieval, the cryptographic proof of which tenants' embeddings were included and which were filtered, allowing post-hoc audit.
Condition of Validity
* **Applies when:** Multiple tenants share the embedding space, tenants can inject content, and retrieval can return cross-tenant results. * **Does not apply when:** Tenants use strictly isolated embedding spaces, or all content is pre-vetted and trusted across tenants.
Decision Boundary
- **Is the embedding space shared across tenants?** If no, standard per-tenant retrieval applies. If yes, proceed.
- **Is the `tenant_origin_receipt` present for each embedding?** If no, the retrieval layer cannot filter by tenant. The embedding must be quarantined.
- **Does the `tenant_isolation_policy_signature` permit cross-tenant retrieval for this query?** If yes, the query can return cross-tenant results. If no, the retrieval must be filtered to the querying tenant only.
- **Conversely, when can cross-tenant retrieval proceed without adversarial screening?** Only when the tenants have explicitly opted in to shared retrieval for the query domain, and all participating tenants are known and trusted.
Operational Effect
- **Cross-Tenant Contamination Prevention:** Filters embeddings by tenant origin, preventing malicious tenants from hijacking other tenants' retrievals.
- **Injection Detection:** Screens embeddings for adversarial characteristics before indexing.
- **Audit Trail:** Preserves per-retrieval attribution, enabling investigation of any suspicious patterns.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class TenantVerdict(Enum):
TENANT_ISOLATED_RETRIEVAL = auto()
CROSS_TENANT_PERMITTED = auto()
CROSS_TENANT_BLOCKED = auto()
UNSAFE_EMBEDDING_QUARANTINED = auto()
@dataclass(frozen=True)
class EmbeddingRecord:
embedding_id: str
tenant_origin: str
tenant_origin_receipt: str | None
adversarial_screen_passed: bool
@dataclass(frozen=True)
class TenantQuery:
querying_tenant: str
target_domain: str
opt_in_cross_tenant: bool
@dataclass(frozen=True)
class TenantPolicy:
trusted_tenants_for_cross_retrieval: frozenset[str]
require_adversarial_screen: bool
@dataclass(frozen=True)
class TenantManifest:
verdict: TenantVerdict
directive: str
manifest_digest: str
class SharedEmbeddingInjectionGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_retrieval(
cls, record: EmbeddingRecord, query: TenantQuery, policy: TenantPolicy
) -> TenantManifest:
# Gate 1: Origin receipt
if not record.tenant_origin_receipt:
return TenantManifest(
verdict=TenantVerdict.UNSAFE_EMBEDDING_QUARANTINED,
directive=f"HALT: Embedding {record.embedding_id} lacks tenant origin receipt. Quarantine.",
manifest_digest=cls._hash(f"NO_ORIGIN:{record.embedding_id}")
)
# Gate 2: Adversarial screen
if policy.require_adversarial_screen and not record.adversarial_screen_passed:
return TenantManifest(
verdict=TenantVerdict.UNSAFE_EMBEDDING_QUARANTINED,
directive=f"HALT: Embedding {record.embedding_id} failed adversarial screen.",
manifest_digest=cls._hash(f"FAILED_SCREEN:{record.embedding_id}")
)
# Gate 3: Same tenant
if record.tenant_origin == query.querying_tenant:
digest = cls._hash(json.dumps({
"embedding": record.embedding_id,
"tenant": query.querying_tenant,
"mode": "SAME_TENANT"
}, sort_keys=True))
return TenantManifest(
verdict=TenantVerdict.TENANT_ISOLATED_RETRIEVAL,
directive=f"PROCEED: Same-tenant retrieval for {record.embedding_id}.",
manifest_digest=digest
)
# Gate 4: Cross-tenant
if not query.opt_in_cross_tenant:
return TenantManifest(
verdict=TenantVerdict.CROSS_TENANT_BLOCKED,
directive=f"BLOCKED: Cross-tenant retrieval not opted in for {query.querying_tenant}.",
manifest_digest=cls._hash(f"NO_OPT_IN:{query.querying_tenant}")
)
if record.tenant_origin not in policy.trusted_tenants_for_cross_retrieval:
return TenantManifest(
verdict=TenantVerdict.CROSS_TENANT_BLOCKED,
directive=f"BLOCKED: Tenant {record.tenant_origin} not in trusted cross-retrieval list.",
manifest_digest=cls._hash(f"UNTRUSTED_TENANT:{record.tenant_origin}")
)
# Gate 5: Permitted
digest = cls._hash(json.dumps({
"embedding": record.embedding_id,
"origin_tenant": record.tenant_origin,
"querying_tenant": query.querying_tenant,
"domain": query.target_domain
}, sort_keys=True))
return TenantManifest(
verdict=TenantVerdict.CROSS_TENANT_PERMITTED,
directive=f"PROCEED: Cross-tenant retrieval permitted from {record.tenant_origin}.",
manifest_digest=digest
)