Skip to content

fix(security): three paths that read an absent workspace as every workspace - #405

Open
jwvanderstam wants to merge 1 commit into
mainfrom
fix/list-documents-tool-scope
Open

jwvanderstam wants to merge 1 commit into
mainfrom
fix/list-documents-tool-scope

Conversation

@jwvanderstam

Copy link
Copy Markdown
Owner

I found three cross-workspace disclosures while sorting the workspace_id: str | None database 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.

# Path Who could reach it What leaked Evidence
1 POST /api/chat additional_workspace_ids any user or workspace API key that knows a workspace's UUID document chunks and long-term memories from that workspace, via the prompt, the answer and sources [repro] at the route layer: a foreign id reached retrieve_context, and the request was answered with 200
2 POST /api/documents/test any viewer 200-character previews of the best-matching chunks from every workspace, with filename and page [code]
3 list_documents LLM tool (tool calling is on by default) any chat user every workspace's filenames, chunk counts and upload dates [code]

Fixes

  1. New check_additional_workspace_access() in security_fastapi.py, wired into chat through _authz.deny_additional(). Each extra id needs:

    • for a user, membership at viewer or above;
    • for an admin, nothing: any id is allowed;
    • for a workspace API key, nothing works: a key may name no extra workspace, which makes the promise in WORKSPACE_API_KEYS.md true.

    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.

  2. The diagnostics route passes get_workspace_id(request), as its neighbouring routes do.

  3. list_documents reads current_request_scope(), as its sibling search_documents has 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 to Scope in P2-1b-ii closes the remaining instances of the pattern.

Verification (Python 3.12 container, isolated from the live stack)

  • ruff, mypy, bandit: clean.
  • Fast suite: 3192 passed, 0 failed.
  • test_object_authorization_over_the_wire.py and test_document_routes.py against a throwaway pgvector pg16: 89 passed, 0 skipped.
  • With each fix reverted, its own tests fail. That's 5 new tests in all; the route-level chat test fails with 200 == 403.

Also found, not fixed here (off by default)

With AGGREGATOR_AGENT_ENABLED=true, AggregatorAgent runs ToolRouter._local_docs in a ThreadPoolExecutor without copying the context, so current_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

…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
jwvanderstam force-pushed the fix/list-documents-tool-scope branch from 3ab8447 to 07b08d0 Compare October 1, 2026 18:00
Copilot AI balanced review requested due to automatic review settings October 1, 2026 18:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonarqubecloud

sonarqubecloud Bot commented Oct 1, 2026

Copy link
Copy Markdown

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.

2 participants