Summary
The MCP list_documents tool can disclose metadata for private documents that the caller should not be able to access.
When the query argument exactly matches a document slug or urlId, the handler performs a direct document lookup and prepends the result to the response without enforcing the normal object-level read authorization check. As a result, a low-privileged user with a read-scoped MCP/OAuth/API credential can obtain metadata for private documents outside their permission set if they know a valid slug or urlId.
This appears to affect the current main branch and the latest release code path I reviewed (v1.7.1).
Details
The issue is in the MCP implementation of list_documents.
The normal search flow delegates to the access-filtered search provider, which is consistent with the tool description that it searches documents the user has access to. However, there is a separate exact-match branch for query values that look like a document slug or urlId.
In that branch, the handler directly resolves the document with:
Document.findByPk(query, { userId: user.id })
and then prepends the returned document to the result list.
The problem is that this lookup is treated as if it were an authorization filter, but it is not. Based on the code path I reviewed, Document.findByPk(..., { userId }) preloads membership-related information but does not itself enforce the document read policy. The exact-match result is then serialized and returned without an explicit authorize(user, "read", document) check.
This creates an authorization bypass in the MCP read surface:
- the access-filtered search provider may return no authorized hits
- but a valid exact slug /
urlId can still cause the private document to be returned
The exposed response is metadata, not full document content. Based on the presenter path used here, the returned fields can include document identifiers and descriptive metadata such as title, summary, urlId, collectionId, createdBy, updatedBy, collaborator-related fields, source metadata, and timestamps.
This is not just a UI inconsistency or search quirk. The tool explicitly promises access-filtered results, and the sibling collection tool appears to apply an additional authorization-aware exact-match filter rather than trusting the raw lookup result.
PoC
I validated this with a focused local test harness that exercised the production MCP handler path.
Preconditions
- MCP is enabled.
- The attacker has a valid low-privileged credential that is allowed to call MCP tools with read scope.
- There exists a private document that the attacker should not be able to access.
- The attacker knows a valid slug or
urlId for that private document.
Reproduction
Send a request to the MCP endpoint using the low-privileged credential:
POST /mcp/
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_documents",
"arguments": {
"query": "<private-document-slug-or-urlId>"
}
}
}
Expected result
The tool should only return documents that the caller is authorized to read. If the caller does not have access to the target document, the response should not include it.
Observed result
When query exactly matches the target document slug or urlId, the document is returned by the exact-match branch even when the normal authorized search path would return no results.
In my focused validation, the search provider returned an empty authorized result set, and the exact-match path still produced a returned document object without an explicit document-level read authorization check.
Control example
Using the same request shape with a nonexistent slug or unrelated identifier does not return the target document.
Impact
This issue allows a low-privileged MCP/OAuth/API user to retrieve metadata for private documents outside their permission set if they know a valid slug or urlId.
The impact is a clear object-level access control failure on a read surface. Even without full document body disclosure, the leaked metadata can expose sensitive document existence and context, including internal titles, summaries, identifiers, ownership/collaboration signals, and related collection metadata.
At minimum, this is an unauthorized private document metadata disclosure. Depending on deployment patterns and how easily valid slugs or urlId values can be learned from other surfaces, this may also increase the risk of targeted enumeration of sensitive internal documents.
Suggested fix
Do not treat Document.findByPk(..., { userId }) as an authorization decision in the exact-match branch.
A safe fix would be to require the same effective authorization guarantees as the normal search path before returning the exact-match document, for example by:
- explicitly enforcing
read authorization on the resolved document before serializing it, or
- restricting exact-match results to the same access-filtered result set used by the normal search flow
Notes
I have intentionally omitted local filesystem paths, local token values, and real document identifiers from this report. I can provide a minimal sanitized test snippet or additional validation details if helpful.
Summary
The MCP
list_documentstool can disclose metadata for private documents that the caller should not be able to access.When the
queryargument exactly matches a document slug orurlId, the handler performs a direct document lookup and prepends the result to the response without enforcing the normal object-levelreadauthorization check. As a result, a low-privileged user with a read-scoped MCP/OAuth/API credential can obtain metadata for private documents outside their permission set if they know a valid slug orurlId.This appears to affect the current
mainbranch and the latest release code path I reviewed (v1.7.1).Details
The issue is in the MCP implementation of
list_documents.The normal search flow delegates to the access-filtered search provider, which is consistent with the tool description that it searches documents the user has access to. However, there is a separate exact-match branch for
queryvalues that look like a document slug orurlId.In that branch, the handler directly resolves the document with:
Document.findByPk(query, { userId: user.id })and then prepends the returned document to the result list.
The problem is that this lookup is treated as if it were an authorization filter, but it is not. Based on the code path I reviewed,
Document.findByPk(..., { userId })preloads membership-related information but does not itself enforce the documentreadpolicy. The exact-match result is then serialized and returned without an explicitauthorize(user, "read", document)check.This creates an authorization bypass in the MCP read surface:
urlIdcan still cause the private document to be returnedThe exposed response is metadata, not full document content. Based on the presenter path used here, the returned fields can include document identifiers and descriptive metadata such as title, summary,
urlId,collectionId,createdBy,updatedBy, collaborator-related fields, source metadata, and timestamps.This is not just a UI inconsistency or search quirk. The tool explicitly promises access-filtered results, and the sibling collection tool appears to apply an additional authorization-aware exact-match filter rather than trusting the raw lookup result.
PoC
I validated this with a focused local test harness that exercised the production MCP handler path.
Preconditions
urlIdfor that private document.Reproduction
Send a request to the MCP endpoint using the low-privileged credential:
Expected result
The tool should only return documents that the caller is authorized to read. If the caller does not have access to the target document, the response should not include it.
Observed result
When
queryexactly matches the target document slug orurlId, the document is returned by the exact-match branch even when the normal authorized search path would return no results.In my focused validation, the search provider returned an empty authorized result set, and the exact-match path still produced a returned document object without an explicit document-level
readauthorization check.Control example
Using the same request shape with a nonexistent slug or unrelated identifier does not return the target document.
Impact
This issue allows a low-privileged MCP/OAuth/API user to retrieve metadata for private documents outside their permission set if they know a valid slug or
urlId.The impact is a clear object-level access control failure on a read surface. Even without full document body disclosure, the leaked metadata can expose sensitive document existence and context, including internal titles, summaries, identifiers, ownership/collaboration signals, and related collection metadata.
At minimum, this is an unauthorized private document metadata disclosure. Depending on deployment patterns and how easily valid slugs or
urlIdvalues can be learned from other surfaces, this may also increase the risk of targeted enumeration of sensitive internal documents.Suggested fix
Do not treat
Document.findByPk(..., { userId })as an authorization decision in the exact-match branch.A safe fix would be to require the same effective authorization guarantees as the normal search path before returning the exact-match document, for example by:
readauthorization on the resolved document before serializing it, orNotes
I have intentionally omitted local filesystem paths, local token values, and real document identifiers from this report. I can provide a minimal sanitized test snippet or additional validation details if helpful.