genai
SECURITY LAB
1Part 1 of 7

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.

LLM09 - Weakness 1
The index never checks who's asking
Nearest-neighbour search ranks by cosine distance. With no filter bound to the caller, relevance quietly stands in for permission.
A vector index ranks chunks by similarity to the query - it has no idea which tenant owns a chunk.
genai
SECURITY LAB
There is no exploit in this one. Just a search answering a different question than the one your access control was written for.
0:00
1:45
One index, no caller-bound filter
RELEVANCE STANDS IN FOR PERMISSION - SAME QUERY, ONE CHANGED STRUCTUREONE SHARED INDEXAcme chunkGlobex key: GX-77-Qinternal-only noteNO FILTER BOUND TO THE CALLERACME USER ASKSour migrationkey?index.query(top_k=8)no filterranks by cosine, not ownerGlobex chunk lands in contextGX-77-Q leaked to Acmeauthorization check never ranFILTER BOUND TO THE CALLER, BEFORE RANKINGquery(q, filter={tenant: session.tenant_id})Globex chunk ineligible - never a candidate
Same Acme query, one changed structure. In the flaw the shared index has no filter bound to the caller, so Globex's chunk is a candidate and cosine similarity ranks it into context - the leak happened at retrieval. The fix binds a tenant/ACL filter to the authenticated caller BEFORE ranking, so a foreign chunk is never eligible.

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.

What the index checks

How close a stored chunk is to the question. Two chunks from two different customers are equal candidates if their meaning is equally close.

What the application must add

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.

Key principles

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.

Key points
One shared index across tenants with no per-request tenant filter means an Acme user's query can surface a Globex document. The confidential value sits inside that foreign chunk, so retrieval alone discloses it (Cross-Tenant Retrieval Leak).
Public and internal chunks in the same corpus give the assistant no reliable boundary: a normal docs search can return an article carrying an internal-only note (Public Knowledge Base Contains Internal Note).
A 'public-only' metadata rule is a boundary when the server enforces it as a filter before ranking, with a value the caller cannot change. A tag that only boosts the score, or a filter value taken from the request or the model, leaves the internal chunk reachable.
The fix is an access-control filter applied at query time, bound to the authenticated caller - tenant id, visibility tier, ACL - so the candidate set is already legal before similarity ever runs.
A query-time filter protects only requests that pass through the application. The vector store is a database with its own management API. If that API answers without authentication, a caller can read another tenant's records directly. Require authentication on the store itself, and keep its management endpoints off networks that application users can reach.
Check yourself
Knowledge check
A teammate says: 'I fixed the cross-tenant leak - I told the model in the system prompt to only use chunks whose tenant field matches the caller.' Why does that not close the hole?
Try what you just learned
Free labs need only a sign-in; the rest are on a paid plan.
Go deeper
FAQ
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.

Comments
No comments yet — be the first.
Get the next part

New parts ship regularly. Leave your email and I’ll send each one — no spam, unsubscribe anytime.

© 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.