Skip to content

Scope integration document-search to the integration's workspace - #162

Merged
ccfiel merged 1 commit into
mainfrom
fix/document-search-account-scope
Jul 30, 2026
Merged

Scope integration document-search to the integration's workspace#162
ccfiel merged 1 commit into
mainfrom
fix/document-search-account-scope

Conversation

@ccfiel

@ccfiel ccfiel commented Jul 30, 2026

Copy link
Copy Markdown
Member

Fixes GHSA-fwgr-c6wh-xxff.

Summary

POST /api/integrations/actions/document-search fanned out across every workspace the linked user belonged to. POST /api/integrations/actions/document-read looks only in integration.workspaceId and 404s anything else. Search therefore returned path, title, status, updatedAt and a body snippet for documents the very same caller could not open.

Search now searches integration.workspaceId, exactly like read.

What the advisory got slightly differently

I reproduced both claims before changing anything. The in-workspace half did not reproduce: searchDocuments already builds a WorkspaceResolver for the linked user and drops any document whose resolved role is null, so a private folder is correctly filtered and no snippet is generated. Probe against main, one integration bound to workspace A, linked user also a member of workspace B:

Call Before
document-read, private folder in A 403 — and search correctly omitted it
document-read, document in B 404
document-search returned B's documentops/bravo.md, with snippet "# Bravo zebrafish secret plan"

So the divergence was purely the workspace scope, which is what this changes. The advisory's "expected" is satisfied either way: a document read denies is now absent from search, including its path and snippet.

This narrows a recently shipped behavior — please confirm

The cross-workspace fan-out was deliberate. It arrived with the integration search work and had a test asserting it (document-search: 200 fans out across all user workspaces when user is a member of multiple). That test encoded the vulnerable behavior, so it is rewritten to assert the scoped behavior instead.

If searching a user's other workspaces through one integration is a product requirement, the safe shape is to bound the fan-out by something the integration is actually authorized for rather than by the user's raw membership list, and to widen document-read to match. That is a design change, not a security fix, so I did not attempt it here. Say the word and I will spec it.

The MCP tools are intentionally untouched

pageden_search_documents fans out the same way, but so does pageden_read_document — both span the linked user's workspaces, so on that surface read and search already agree and nothing is searchable-but-unreadable. Narrowing MCP would be the same product decision as above. Flagging it because the exposure shape is similar even though the read/search divergence is not there.

Numbers

  • Server unit: 171 / 171 ✓
  • Integration: 430 / 430 ✓
  • Typecheck: ✓
  • Lint: ✓

Verification that the tests catch the bug

With the route change reverted:

× document-search: stays inside the integration's workspace, matching document-read
  - Expected
  + Received
  +   "cms7defcw000nqbw56tffpkoh"     // the out-of-scope workspace id

Test plan

  • pnpm -r typecheck
  • pnpm -r lint
  • pnpm --filter @pageden/server test
  • pnpm --filter @pageden/server test:integration
  • Manual: with an integration in workspace A and a linked user who also belongs to B, confirm search returns only A's documents and that in-scope search still works.
  • CI: e2e + CodeQL

Follow-ups not in this PR

  • Neither per-user branch (read or search) applies the integration's allowedFolders; only the workspace-level branches do. Consistent between the two, so not a divergence, but worth deciding whether a linked user's own permissions should be bounded by the integration's folder scope.
  • The workspace-level search branch calls searchDocuments with userId: "", which resolves to no membership and therefore returns nothing. Looks like a functional dead end rather than a leak — separate from this advisory.

…rkspace

GHSA-fwgr-c6wh-xxff. POST /api/integrations/actions/document-search fanned
out across every workspace the linked user belonged to, while
document-read looks only in integration.workspaceId and 404s anything
else. Search therefore returned path, title, status and a body snippet for
documents the same caller could not open.

The in-workspace permission filter was never the problem -- searchDocuments
already resolves the linked user's role and drops documents they cannot
read. The divergence was purely the workspace scope, so search now uses
integration.workspaceId, exactly like read.

This narrows the cross-workspace fan-out added in #159's sync of the
integration search work. The equivalent MCP tools are left alone: there
pageden_read_document also spans the user's workspaces, so read and search
already agree and no document is reachable by search but not by read.

Regression tests cover both halves of the advisory: a document in another
workspace that read 404s never appears in search, and a document the
linked user lacks permission on is returned neither as a result nor as a
snippet.
@ccfiel
ccfiel merged commit 85774e3 into main Jul 30, 2026
10 checks passed
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.

1 participant