Skip to content

Chat IDOR - unauthenticated-of-ownership read, delete, and message injection on any chat #3697

Description

@geo-chen

What happened?

reported via https://github.com/QuivrHQ/quivr/security/advisories/GHSA-w7hf-3v2c-xj58 on 2 June 2026 - no response:

Summary

Three chat endpoints in the quivr backend accept a caller-supplied chat_id and operate on it without verifying that the chat belongs to the requesting user. Any authenticated user can read another user's full conversation history (questions and LLM answers), permanently delete any user's chat, and inject arbitrary messages into any user's chat. An adjacent endpoint in the same controller performs the ownership check correctly, confirming the omission is an oversight rather than a design choice.

Details

File: backend/api/quivr_api/modules/chat/controller/chat_routes.py

Vulnerable endpoints (no current_user / ownership scoping):

  1. GET /chat/{chat_id}/history
    Returns the full conversation for the supplied chat_id. The handler and underlying repository filter only on chat_id, never on the authenticated user's id, so any authenticated caller receives any chat's history (all user questions and all LLM answers, which routinely contain sensitive retrieved knowledge).

  2. DELETE /chat/{chat_id} (lines ~89-97)

    @chat_router.delete(
        "/chat/{chat_id}", dependencies=[Depends(AuthBearer())], tags=["Chat"]
    )
    async def delete_chat(chat_id: UUID, chat_service: ChatServiceDep):
        """Delete a specific chat by chat ID."""
        chat_service.delete_chat_from_db(chat_id)
        return {"message": f"{chat_id}  has been deleted."}
    

    The signature has no current_user parameter; the service deletes purely by chat_id.

  3. POST /chat/{chat_id}/question/answer
    Persists a message into the chat identified by chat_id with no ownership check, allowing an attacker to inject fabricated question/answer pairs into a victim's conversation.

Correctly-gated contrast in the same file: the adjacent update_chat_metadata_handler (lines ~101-120, PUT /chat/{chat_id}/metadata) loads the chat and verifies ownership against the authenticated user before mutating it. The three endpoints above omit that same check.

Root cause: AuthBearer() establishes that the caller is authenticated but does not bind the chat_id to the caller. The chat repository queries are keyed on chat_id alone (no AND user_id = :current_user), so the object reference is fully user-controlled.

PoC

Prerequisites: a running quivr backend (official image stangirard/quivr-backend-prebuilt:latest was used) with two registered users A and B, each with a bearer token. User A has created at least one chat; note its chat_id (UUIDs are exposed to the owner and are guessable/enumerable as version-4 UUIDs but are typically obtained from referer leakage, shared links, or logs).

  1. As victim A, create a chat and send a question so a history row exists. Record CHAT_A.

  2. As attacker B, read A's history:

    GET /chat/CHAT_A/history
    Authorization: Bearer <token_B>

    Observed: HTTP 200 with the full JSON conversation belonging to user A (questions and LLM answers). Evidence: screenshots/idor_chat_history_response.json.

  3. As attacker B, delete A's chat:

    DELETE /chat/CHAT_A
    Authorization: Bearer <token_B>

    Observed: HTTP 200, {"message":"CHAT_A has been deleted."}, no 403. The chat is gone for user A. Evidence: screenshots/idor_delete_chat_response.txt.

  4. As attacker B, inject a message:

    POST /chat/CHAT_A/question/answer
    Authorization: Bearer <token_B>
    { ... fabricated question/answer ... }

    Observed: the fabricated entry is persisted into A's conversation.

Validation level: live end-to-end against the official Docker image with a local pgvector PostgreSQL; steps 2 and 3 returned HTTP 200 cross-user. A standalone code-path reproducer is at scripts/chat_idor_standalone.py.

Impact

Authenticated regular user (PR:L). Confidentiality: full disclosure of any other user's chat history, including the LLM answers that embed content retrieved from that user's private knowledge bases. Integrity: permanent deletion of any chat and injection of attacker-authored messages into any conversation. No special role required; chat_id is the only thing the attacker supplies.

Relevant log output

Twitter / LinkedIn details

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: backendRelated to backend functionality or under the /backend directorybugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions