Repository navigation
test(security): R-5b — every use of ALL_WORKSPACES carries a reason - #528
Merged
Merged
Conversation
jwvanderstam
added a commit
that referenced
this pull request
Nov 8, 2026
… uses From the separate review of #528: the walk covered src/ only and matched only the bare name. It now walks every directory whose code imports src (src/, mcp_servers/, plugins/), counts scope.ALL_WORKSPACES, and fails on an import that renames it. The two MCP list_sources tools get reasons. Three reasons are corrected: the purge reason no longer cites R-9 (not on main yet), feedback.insert says correct_doc_ids is unchecked, and for_user and any_local_only_sources name the rest of what they do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
Owner
Author
Separate security review (CONTRIBUTING / QP-9)A separate agent session reviewed this PR read-only against Low findings and how they were answered:
🤖 Generated with Claude Code |
ALL_WORKSPACES is the one scope row-level security does not narrow, and both High findings of the QP-9 review were paths that passed it where a workspace user could reach them. A unit test now walks src/ and fails on a function that passes it without a recorded reason, and on a reason whose function no longer passes it. A comparison on the scope is a branch, not a use. The 42 functions that pass it today were each checked against every caller that can reach them: boot and background work, rows that name their own workspace, ids already proved in the caller's scope, the authorisation path itself, and admin-only routes. None let a workspace user reach another workspace's rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
… uses From the separate review of #528: the walk covered src/ only and matched only the bare name. It now walks every directory whose code imports src (src/, mcp_servers/, plugins/), counts scope.ALL_WORKSPACES, and fails on an import that renames it. The two MCP list_sources tools get reasons. Three reasons are corrected: the purge reason no longer cites R-9 (not on main yet), feedback.insert says correct_doc_ids is unchecked, and for_user and any_local_only_sources name the rest of what they do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
jwvanderstam
force-pushed
the
test/r5b-all-workspaces-reasons
branch
from
November 8, 2026 21:35
b61d277 to
c4149bd
Compare
This was referenced Nov 8, 2026
jwvanderstam
added a commit
that referenced
this pull request
Nov 11, 2026
…t a workspace (#532) A global contribution and a document uploaded without a workspace both have workspace_id NULL. documents.exists(filename, None) matched both, so an admin upload naming no workspace, an admin's workspace-less connector, or the re-ingest worker could update a contribution in place, and every workspace reads the tier. documents.stale() also returned contributions, which have no source file to re-read. Both now skip rows with contributed_at set. Found by the separate review of #528. The integration test fails on the old code with (True, True). Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts were side-by-side additions to CHANGELOG, file-map and the docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jwvanderstam
added a commit
that referenced
this pull request
Nov 11, 2026
…me 403 (#534) POST /api/connectors/{id}/webhook is public, and answered 404 for an unknown connector and 400 for one of another type before checking the secret, so an anonymous caller could learn which connector ids exist and which are webhooks. A missing connector, another type, a connector without a secret and a wrong secret now all return the same 403. Found by the separate review of #528. The parametrised test fails on the old code for the missing and wrong-type cases. Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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.



What
QP-9 review finding R-5, its open half (R-5b): a ratchet over the
ALL_WORKSPACEScall sites, each with a recorded reason.Change
tests/unit/test_all_workspaces_has_a_reason.pywalks the AST ofsrc/and collects every function (module:qualname) that passesALL_WORKSPACESas a value. Comparisons (scope is ALL_WORKSPACES) are branches, not uses, and are left out. The test fails on:REASONS;No
src/change.The audit behind the 42 reasons
Each function was checked against every caller that can reach it, not only the obvious one. They fall into these groups:
mark_ingested, the reranker exportconnectors.create,conversations.create*,memories.insert,ingest_document,documents.existsfeedback.insert(R-8),memories.record_use,mark_extracted,_touch,contributions.archiveworkspace_keys.resolve,members.role/for_user, an admin naming no workspace, the webhook receiver (id + mandatory secret)members.add/removepurge_all, user and workspace lists, feedback stats, GKB contribute and list, admin stats (R-6)any_local_only_sources, which can only block the cloud fallbackNothing was found that lets a workspace user reach another workspace's rows. Two observations, both outside this PR:
WorkspacesService.visible_toand the services'_actor()helpers still special-case a user id of"anonymous", the RBAC bypass's identity. No guard produces it any more, so the branch is dead code. Left as is (pre-existing).feedback.insertwritesanswer_feedbackwithout aworkspace_id. Not a leak, since the ids are checked first (R-8), but the row carries no workspace of its own.Tests
main'ssrc/.ALL_WORKSPACESuse makes the first test fail with['zz_probe.py:leak'].test_docs_reference_what_existspasses with the new references.Docs
SECURITY.mdR-5 row, CHANGELOG, ROADMAP (item 4 ✅),architecture.md(the rule now points at the test), file-map. CHANGELOG and ROADMAP will need a trivial rebase against the other open QP-10 PRs.🤖 Generated with Claude Code
https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx