FeaturesSecurityPricingCompareDevelopersBlogStart Free
BlogAugust 4, 2026

Can your data room AI see what you cannot?

A permission filter can live in three places, and two of them are not controls. Here is how to tell which one a vendor has built, the query predicate we use, and what an adversarial caller receives.

Three places a filter can live

Every AI data room applies a permission filter somewhere. Vendors describe all three the same way, so the wording tells you nothing and the layer tells you everything.

  • Inside the retrieval queryenforced

    The permission check is a predicate in the WHERE clause. An unauthorised chunk is never selected, so there is no downstream code path that could return it.

  • After retrieval, in application codebypassable

    The rows were already read. Every new caller, every error branch, every log line and every cache is a place the unfiltered result can escape through.

  • As an instruction in the promptbypassable

    The restricted text is already in the context window. Asking a probabilistic system not to use it is a preference, not a boundary.

The same filter, three placements. Only the first one is a control.

The distinction is not theoretical. In July 2026 we found the second failure in our own system: a signed-in user with no relationship to a room called the retrieval function with another participant's identifier and received 30 chunks across 13 documents, containing verbatim deal text up to 1,952 characters. The in-query predicate was correct. The identifier it trusted was not, because the function could be called directly by any authenticated role. Both halves have to hold.

Method

The subject is rag_vector_search, the function every chat answer retrieves through. Its permission logic is two predicates in one WHERE clause:

WHERE c.tenant_id = p_tenant_id
  AND d.deleted_at IS NULL
  AND d.status = 'ready'
  AND (d.ai_chat_enabled IS NOT FALSE)
  AND (1 - (c.embedding_voyage <=> p_query_embedding)) >= p_similarity_threshold
  AND participant_can_access_document(p_participant_id, c.document_id)
ORDER BY c.embedding_voyage <=> p_query_embedding
LIMIT p_limit;

participant_can_access_documentresolves the participant's group defaults, walks up the folder tree for inherited folder permissions, then applies any document-level override, and returns true only at view level or above. The tenant predicate is separate and deliberate: it holds even if the permission resolver is wrong.

  1. Question
    Participant identity comes from the session, never the request body
  2. Embed
    Query vector only, no document content
  3. Retrieve
    Tenant and participant predicates inside the SQL
  4. Answer
    Model sees authorised chunks only
  5. Cite
    Page or cell range on every claim
A chat request. The permission boundary is at step three, before any document text exists in the process.

Three adversarial cases, and one more the July incident added:

  1. The room's own participant asks a question. Baseline.
  2. A participant of a different room in the same tenant, substituted into the same call.
  3. A participant of a different tenant entirely.
  4. The published anonymous key, calling the function directly over the REST interface with no session at all.

Reproducing it

Case four needs nothing but the public key that ships in the browser bundle, so anyone can run it against us:

node --experimental-strip-types tests/security/org-isolation.test.ts
node scripts/security/check-function-privileges.mjs --require-db

The first sends a zero vector and a nil UUID straight at the REST interface and counts only SQLSTATE 42501as a pass, so an ambiguous error is scored as a failure rather than quietly counted as a win. The second reads the deployed function's source out of pg_proc and fails if either predicate is missing from the text, or if anon or authenticated has regained execute permission. Both run in CI on every push.

Results

30
Chunks for the room's own participant
The retrieval limit, p_limit = 30
0
Chunks for a foreign participant
participant_can_access_document predicate
0
Chunks for another tenant
c.tenant_id = p_tenant_id predicate
Adversarial callers against rag_vector_search, and how each result is established.
CriterionResultEnforced byHow verified
Room's own participantUp to 30 chunksRetrieval limitProduct behaviour
Participant of another room0 chunksPermission predicate in the querySource assertion in CI
Participant of another tenant0 chunksTenant predicate in the querySource assertion in CI
Published anonymous keyCall refused, 42501Execute grant revokedLive probe, 13 of 13 passing
Same call before July 202630 chunks, 13 documentsNothingThe incident that prompted the lockdown
Adversarial callers against rag_vector_search, and how each result is established.

The last row is the one worth reading twice. The predicate that makes rows two and three zero was already in the query when the incident happened. It was not enough on its own, because the caller controlled the participant identifier the predicate was checking. The fix was to revoke direct execute permission from every client-reachable role, so the only way to reach the function is through a server that derives the participant from the session.

One consequence worth naming. Because the fence is a predicate rather than a post-filter, an answer can only cite what the participant could already open, which is why every citation is checkable by the person who received it. That is covered in the citations piece.

Related: What a citation has to point at · How we test our own data room · Do watermarks survive screenshots · Pricing

Permission-fenced AI with cited answers is included in every paid tier rather than sold as an add-on. See the tiers.

FAQ

Questions about permission-fenced AI.

AI retrieval that returns only the documents the asking participant is already allowed to open. Sifrsys enforces this at the database level: the permission check is a predicate inside the retrieval query itself, so an unauthorised chunk is never selected, not selected and then hidden.
Because the rows were already read. Every path that forgets to apply the filter, every new caller, every error branch that returns the raw result, and every place the text is logged or cached becomes a way out. The filter is correct only where nothing else can be added downstream of it.
No. A system prompt that says to answer only from permitted documents is a request to a probabilistic system that has already been handed the text. If the restricted content is in the context window, the control has already failed, whatever the model does next.
Ask which layer the check runs in and ask to see it. A vendor whose filter is in the query can point at the predicate. Then ask what a caller who substitutes another participant's identifier receives, and what the retrieval function's execute grants are. If the answer is that the application never sends the wrong identifier, the control is the application, not the database.
Start Free