Cross-tenant and access-control leakage
The most damaging failure of a RAG pipeline: retrieval returns a chunk the asking user was never entitled to see. A vector index is just a similarity search over a pile of embeddings - if every tenant's and every visibility tier's content lands in the same pile with no filter bound to the request, then any query that is semantically close to a confidential chunk pulls it into the model context. The authorization decision was never made; relevance silently stood in for permission.
How a RAG pipeline decides what the model reads
Most assistants that answer from company documents use retrieval-augmented generation, or RAG. The model does not know your documents. The application looks up the passages that seem relevant to a question, places them in the model's context, and asks the model to answer from them.
The lookup has four steps, and you need all four to follow this series.
- Chunk. Each document is cut into passages of a few hundred words. A passage is called a chunk.
- Embed. Each chunk is turned into an embedding, a long list of numbers that places text with a similar meaning close together.
- Index. The embeddings are stored in a vector index, a database built to find the stored vectors nearest to a given one. Each entry can carry metadata, such as the owner, the source or a visibility label.
- Retrieve. When a question arrives it is embedded the same way. The index returns the closest chunks, usually a fixed number called top-k, and the application puts them into the context.
Nothing in those steps asks who is asking. The index answers one question: which stored text is closest in meaning to this text. It was built for that and it is good at it. It has no notion of a tenant, a role or a permission unless the application adds one to the query.
This is LLM09, Vector and Embedding Weaknesses, in the OWASP 2026 numbering. The first weakness on the path is the most damaging one: retrieval returns a chunk the person asking was never entitled to see.
Relevance is not permission
A web tester already knows this bug under another name. An endpoint that returns any record whose id you supply, without checking that the record is yours, has broken object-level authorization. A retriever that returns any chunk close to your question, without checking that the chunk is yours, has the same flaw. The only difference is how you address the record. You do not need its id. You need a question that is about the same thing.
Teams miss this because the pipeline looks safe in normal use. People ask about their own work, so the nearest chunks are usually their own. The developer sees correct answers in testing and concludes that ranking keeps customers apart. Ranking never promised that. A question that is on topic for someone else's document will retrieve that document.
The disclosure happens at retrieval. Once a foreign chunk is in the retrieved set it is in the model's context, and often in logs and citations, before the model has written a word. A system prompt that tells the model to discuss only the caller's workspace is asking it to enforce a boundary it cannot see, on data it has already been handed.
How close a stored chunk is to the question. Two chunks from two different customers are equal candidates if their meaning is equally close.
A filter taken from the authenticated session, such as the tenant id and the visibility tier, applied to the search itself so that only chunks the caller may read are candidates at all.
The four labs in this lesson show four places that filter goes missing: it was never applied, it was applied with a value the model chose, the sensitive text was never separable in the first place, and the filter was sound but the store behind it had another door.
Corva: one index, no scope on the search
The Cross-Tenant Retrieval Leak (Real Vector Store) lab (Intermediate) gives you Corva, a workspace knowledge assistant at corva.app that serves many customers from one retrieval stack. You are signed in to the Harborline Freight workspace. Meridian Clinical is another customer on the same platform.
Every customer's chunks sit in the same index, and the search that runs for your question is not restricted to your workspace. Whether you receive Harborline's documents or someone else's depends only on what your question is close to.
The class of move is to ask about the other tenant's subject matter. You are not persuading the model of anything and there is no instruction to override. You are steering the similarity search, so think about what kind of internal document another customer would hold and what a question about it would sound like. When you test, read the retrieved sources as well as the answer. A source that belongs to a workspace you are not in is the finding, whatever the assistant then says about it.
Completion here is by Submit Evidence. You read a confidential value out of the foreign document and submit it. The lab's own hints narrow the search if you get stuck.
A prompt rule cannot repair this. Telling Corva to answer only about the caller's workspace leaves the foreign chunk in its context. The rule depends on the model noticing ownership from the text alone and declining every time. The retriever has already made the disclosure by then.
Cindra: a scope the model was asked to choose
The RAG Retrieval-Time ACL Bypass via Tenant-Filter Stripping lab (Advanced) looks like the fixed version of Corva. Cindra, at cindra.app, does scope every search to a workspace. You are signed in to Brightmark Retail, and Aldergrove Legal is another customer.
The flaw is where the scope comes from. Before searching, Cindra runs a routing step in which a model reads your request and decides which workspace to search. The application then uses whatever that step returns. The tenant boundary has become a model output, produced from text you wrote.
You met this pattern in the prompt injection path. Anything a model derives from untrusted input can be steered by that input. The class of move is to make a wider scope look like the correct routing decision for your request. Your first objective is to work out that a routing step exists and what it is deciding. Compare how Cindra behaves for an ordinary question about your own workspace with how it behaves when your request gives it a reason to look elsewhere. The lab's hints help with the framing. The lesson does not hand you one.
The design lesson is general. An access-control decision must be computed by code from the authenticated session. It must never be generated, and that includes filters a model writes into a tool call, a query plan or a structured output.
DocsSearch AI: internal text inside a public article
The Public Knowledge Base Contains Internal Note lab (Beginner) is the gentlest of the four and it teaches a different point. DocsSearch AI answers customer questions from a public help centre. Every article in its corpus is published. One of them still carries a staff-only note that was never removed before the article was indexed.
No tenant boundary is crossed here and no filter is evaded. The assistant retrieves a public article for an ordinary question and reads back what the article contains, note included. A query-time visibility filter would not help, because the filter works on whole chunks and this chunk is labelled public.
The class of move is ordinary use with attention. Ask the kinds of question a customer would ask and read the answers for content that is plainly written for staff. As a tester, treat this as the first thing to check on any documentation assistant, because it needs no technique at all. Completion is by Submit Evidence.
The control sits at ingestion. Access labels are only as good as the content they are attached to, so review what goes into a public corpus and keep internal material in a separate one.
Sable: a correct filter and an open back door
The Direct Vector-Store Dump (Exposed Ops API) lab (Advanced) removes the application bug. Sable, at sable.app, filters every search to the caller's workspace in code. You are signed in to Northwind Retail and no phrasing gets another customer's document out of the assistant. That part holds, and the lab expects you to confirm it.
A vector index is a database. Like other databases it has a management interface for operators, used to list collections and inspect stored entries. In this deployment that interface is reachable from where you sit and asks for no credentials. The application's filter is irrelevant to a caller who never goes through the application.
The class of move is the one you would use against any datastore: stop testing the front end and look at what else is exposed around it. Work out what the store's own interface offers, what it reveals about how tenants are laid out, and what it returns when asked for stored entries directly. Completion is by Submit Evidence.
The same pattern has leaked data between users of a production assistant with no model involved. In March 2023 a bug in a caching library used by ChatGPT showed some users the conversation titles of others. See the ChatGPT Redis cross-user leak. Raw vectors are also worth protecting on their own account: researchers have reconstructed short passages of text from nothing but their embeddings, as described in Vec2Text: reconstructing private text from its embedding. An exposed index is a data breach even if the original documents are stored elsewhere.
The shared root cause
In all four targets the decision about who may read a piece of text was left to something that cannot make it. Corva left it to ranking. Cindra left it to a model. DocsSearch AI left it to a label on content nobody had reviewed. Sable made the decision correctly in the application and then left a second route to the same data without any check.
When you assess a RAG feature, ask the questions you would ask of any data-access layer. Where is the caller's identity bound to the query? Who or what supplies the filter value? What is actually inside the chunks the filter lets through? What else can reach the store?
The fix that holds
- Filter at query time, from the session. Apply the tenant id and visibility tier as a filter on the search, taken from the authenticated session by application code, so the candidate set is already legal before similarity runs. In code it is one extra argument, something like
filter={"tenant": session.tenant_id}, and it must be present on every search. - Never let a model or the request supply the scope. A model can decide what to search for. It must not decide whose data to search. Ignore or reject any scope value that arrives in user text or model output.
- Prefer physical separation for strong boundaries. A separate index or namespace per tenant, selected by code, fails closed when a filter is forgotten. A shared index with a metadata filter fails open.
- Control what is ingested. Keep internal and public material in separate corpora and review public content before indexing. A filter cannot split a chunk.
- Secure the store as a database. Require authentication and per-tenant authorization on the vector store itself, and keep its management interface off any network an application user can reach.
- Treat prompt rules and output checks as a second layer. They can reduce what a model repeats. They do not undo a retrieval that should not have happened.
What you should be able to do now. Explain why a vector index cannot enforce access by itself, and name the missing control in each target before you send a message. Corva never scopes the search. Cindra scopes it with a value a model chose. DocsSearch AI indexed internal text inside a public article. Sable scopes correctly and leaves the store's own interface open. Complete the Lab for each one, then choose the control that makes the retrieved set legal before the model sees it.
Relevance is not permission - with no caller-bound filter, the index ranks a foreign secret exactly like your own document.
Apply the access filter at query time, bound to the authenticated caller, so illegal chunks are ineligible before similarity ranks.
A higher similarity threshold only measures how close a chunk is. A foreign chunk that closely matches the query still clears it.
A 'public' tag that only boosts the score is a preference. The internal chunk stays in the candidate set.
What is cross-tenant data leakage in a RAG application?
Cross-tenant leakage is when a retrieval-augmented assistant returns a document chunk that belongs to a different customer or user than the one asking. It happens when several tenants share one vector index and the search is not filtered by the caller's identity, so any question close in meaning to another tenant's document retrieves it. OWASP covers it under LLM09, Vector and Embedding Weaknesses.
Why can't a vector database enforce access control by itself?
A vector index answers one question: which stored text is closest in meaning to the query. It ranks by similarity and has no concept of a tenant, a role or a permission unless the application adds a filter to the search. Without that filter, every stored chunk is a candidate for every query, whoever sent it.
Can a system prompt stop an assistant from revealing another tenant's documents?
No. By the time the model reads the system prompt, the retriever has already placed the other tenant's chunk in its context, and often in logs and citations too. The rule asks the model to enforce a boundary it cannot see on data it has already been given. The control has to be a filter on the search, set by application code from the authenticated session.
Should an LLM be allowed to choose the tenant or metadata filter for a search?
No. A filter value that a model produces from the user's request can be steered by that request, which turns the access boundary into something the user can influence. A model may decide what to search for, but the tenant and visibility scope should be computed by code from the session and applied to every retrieval path.
© 2026 GenAI Security Lab. All rights reserved. You may read, quote, and link to this material with attribution. Copying, republishing, redistribution, resale, or use to train models or build competing products is prohibited without prior written permission.