fix(security): three paths that read an absent workspace as every workspace - #405
Open
jwvanderstam wants to merge 1 commit into
Open
jwvanderstam wants to merge 1 commit into
jwvanderstam wants to merge 1 commit into
Conversation
This was referenced Sep 29, 2026
…kspace Found while sorting the workspace_id: str | None methods for P2-1b-ii. - POST /api/chat: additional_workspace_ids from the request body reached document and memory retrieval unauthorised. Any user who knew a workspace's id could retrieve from it, a workspace API key included. Each extra id is now authorised like the primary one (member at viewer+, admin any, API key none); one unauthorised id refuses the request with 403. - POST /api/documents/test: retrieval ran with no workspace, returning chunk previews from every workspace to any viewer. Now uses the authorised one. - list_documents LLM tool: called get_all_documents() bare, listing every workspace's documents. Now reads the request scope like search_documents. Each has a regression test that fails with its fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jwvanderstam
force-pushed
the
fix/list-documents-tool-scope
branch
from
October 1, 2026 18:00
3ab8447 to
07b08d0
Compare
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
All three disclosures are closed with fail-closed authorization and focused regression coverage.
Review effort: Balanced
Findings: None
What changed in this PR
Closes three workspace-isolation gaps in chat, retrieval diagnostics, and LLM document tooling.
Changes:
- Authorizes every additional chat workspace before retrieval.
- Applies request workspace scope to diagnostics and
list_documents. - Adds regression coverage and security documentation.
| File | Description |
|---|---|
src/security_fastapi.py |
Adds authorization for additional workspaces. |
src/routes_fastapi/_authz.py |
Converts authorization denials to responses. |
src/routes_fastapi/api_routes.py |
Guards cross-workspace chat retrieval. |
src/routes_fastapi/document_routes.py |
Scopes diagnostic retrieval. |
src/tools/builtin.py |
Scopes document listing tools. |
tests/unit/test_chat_additional_workspaces_authorised.py |
Tests chat workspace authorization. |
tests/unit/test_fastapi_routes_extended.py |
Tests diagnostic retrieval isolation. |
tests/unit/test_tools_builtin_extra.py |
Tests tool scope enforcement. |
.claude/rules/file-map.md |
Catalogues the new test module. |
docs/ROADMAP.md |
Records the discovered disclosures. |
CHANGELOG.md |
Documents the security fixes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



I found three cross-workspace disclosures while sorting the
workspace_id: str | Nonedatabase methods for P2-1b-ii. Each is fixed here with its own regression test, ahead of the conversion rather than waiting for it. None of them was in the September audit.POST /api/chatadditional_workspace_idssourcesretrieve_context, and the request was answered with 200POST /api/documents/testlist_documentsLLM tool (tool calling is on by default)Fixes
New
check_additional_workspace_access()insecurity_fastapi.py, wired into chat through_authz.deny_additional(). Each extra id needs:WORKSPACE_API_KEYS.mdtrue.One unauthorised id refuses the whole request, so the caller can't mistake a partial answer for a complete one. Unlike
check_workspace_access, it pins nothing, so the request's scope stays the primary workspace.The diagnostics route passes
get_workspace_id(request), as its neighbouring routes do.list_documentsreadscurrent_request_scope(), as its siblingsearch_documentshas since P0-2, and refuses when no scope is bound.Why nothing caught these
All three are the driver-1 shape: a workspace that is left out or never checked reads as "every workspace". None of them addresses an object by path parameter, so the P2-2b matrix can't see them, and none goes through a method in
SCOPED_METHODS, so the AST check doesn't cover them. Converting those methods toScopein P2-1b-ii closes the remaining instances of the pattern.Verification (Python 3.12 container, isolated from the live stack)
ruff,mypy,bandit: clean.test_object_authorization_over_the_wire.pyandtest_document_routes.pyagainst a throwaway pgvector pg16: 89 passed, 0 skipped.200 == 403.Also found, not fixed here (off by default)
With
AGGREGATOR_AGENT_ENABLED=true,AggregatorAgentrunsToolRouter._local_docsin aThreadPoolExecutorwithout copying the context, socurrent_request_scope()raises inside the worker thread. It fails closed, so nothing leaks, but local-docs retrieval probably comes back empty and skips the direct fallback. [code], not reproduced. It deserves its own ticket.Merge order
This touches the same ROADMAP region as #404, so whichever lands second needs a trivial rebase.
🤖 Generated with Claude Code