Skip to content

fix: enforce per-user authorization on /query_multiple - #302

Closed
nangelovv wants to merge 1 commit into
LibreChat-AI:mainfrom
nangelovv:fix/query-multiple-authorization
Closed

nangelovv wants to merge 1 commit into
LibreChat-AI:mainfrom
nangelovv:fix/query-multiple-authorization

Conversation

@nangelovv

Copy link
Copy Markdown

What

Add a per-user authorization filter to POST /query_multiple, matching the ownership check POST /query already performs.

Why

Fixes #301. /query_multiple filtered returned chunks by file_id only, so any authenticated caller could read another tenant's documents by supplying their file_id.

Change

After the similarity search, keep only chunks the requester owns:

authorized_documents = [
    (doc, score)
    for doc, score in documents
    if doc.metadata.get("user_id") in (None, user_authorized)
]

user_authorized is resolved with the existing get_user_id(request) helper. Unowned chunks (user_id is None) remain visible, matching /query. Returns 404 when nothing is authorized; logs the number of withheld chunks.

Testing

  • New tests/test_main.py::test_query_multiple_filters_unauthorized: a result set mixing the caller's chunk and another user's chunk returns only the caller's.
  • tests/test_main.py: 11 passed.

/query_multiple filtered the returned chunks by file_id only, with no user_id
check, so any authenticated caller could read another tenant's document text by
supplying their file_id. The single-file /query endpoint already performs this
ownership check, so /query_multiple was an inconsistent gap.

Filter the results to chunks the requester owns (user_id is None or matches the
authenticated identity), mirroring /query, and return 404 when nothing is
authorized.
@danny-avila

Copy link
Copy Markdown
Collaborator

Closing in favour of #319, which fixes this hole along with the rest of the set it belongs to. Thanks for catching it — the diagnosis here was right, and /query_multiple having no authorization at all was the most serious of them.

Two reasons #319 takes a different shape rather than building on this one:

  • The predicate goes into the store query instead of filtering afterwards. Filtering the returned list still reads the foreign chunks into the process, and they still consume k — so a caller asking for 4 hits can get fewer of their own than they asked for, depending on how another owner's chunks ranked. 🔒 fix: scope every document read and delete by owner #319 pushes (owner, file) into the query before ranking.
  • user_id in (None, user_authorized) keeps null-owner chunks readable by everyone. That is one of the holes 🔒 fix: scope every document read and delete by owner #319 closes, so it treats an absent user_id as owned by nobody, with a backfill statement in the README for deployments holding such rows.

#319 also pairs this with GET /ids, which listed every file id in the deployment — that is the discovery half of the same chain, and closing one without the other leaves the path mostly intact.

@danny-avila danny-avila closed this Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/query_multiple returns documents without an authorization check (cross-tenant IDOR)

2 participants