Summary
A multi-tenant retrieval-augmented generation platform colocated every tenant's embeddings in a single shared vector namespace and relied on a tenant-id metadata filter applied after the similarity search to enforce isolation. A fallback code path — taken when the metadata filter expression failed to parse or when a query exceeded the candidate-set size — returned the raw nearest-neighbour set without the tenant filter. Authenticated tenants received chunks belonging to other tenants in that index.
The leak was deterministic under the trigger condition and required only normal, authenticated API access. No cross-tenant credential was needed: the boundary was a post-filter, not a query constraint, so any failure of the filter degraded to no isolation at all.
Technical Details
Retrieval ran in two stages: an unfiltered approximate-nearest-neighbour search returning top-k candidates, then a metadata predicate (tenant_id == caller) applied in application code to drop foreign rows. When the predicate string was malformed — reachable by supplying certain Unicode in the tenant context, or when k was raised above the configured candidate ceiling — the filter step caught the exception and returned the unfiltered candidate set rather than failing closed. The model then summarised whatever chunks it received, surfacing cross-tenant content in the answer.
# vulnerable pattern — isolation applied AFTER the search, fail-open
def retrieve(query, tenant_id, k=8):
candidates = index.search(embed(query), top_k=k) # all tenants
try:
return [c for c in candidates
if c.metadata["tenant_id"] == tenant_id]
except Exception:
return candidates # <-- fail-OPEN: foreign rows returned
# fixed pattern — isolation pushed INTO the query, fail-closed
def retrieve(query, tenant_id, k=8):
return index.search(
embed(query), top_k=k,
filter={"tenant_id": {"$eq": tenant_id}}, # server-side constraint
) # parse error -> no results, never leakImpact
An authenticated tenant could retrieve and read document chunks belonging to other tenants of the same index, including confidential business content ingested into the platform. Because retrieval results were passed to a model and returned as synthesised answers, exposure could occur without the requesting user ever seeing a raw foreign document — making the leak hard to notice. The defect is a confidentiality breach with a scope confined to tenants sharing a physical index.
Disclosure Timeline
Remediation
The vendor moved tenant isolation into the retrieval query as a server-side filter that fails closed: a malformed or missing tenant constraint now returns zero results rather than an unfiltered set. As defence in depth, tenants with elevated sensitivity are placed in physically separate indexes rather than a shared namespace. Operators of multi-tenant RAG should never enforce isolation in post-query application code, should assert a non-empty tenant constraint on every search, and should add a regression test that confirms a malformed filter returns no rows.