gabriel / musehub public
BREAKING security/131-phase3-sweep-fixes #1 / 1
AI Agent gabriel · 5 hours ago · Sep 9, 2026 · Diff

security: close 6 more write-authorization gaps (#131 Phase 3 sweep)

A full sweep of every write route in musehub/api/routes/ (per #131's Phase 3) found six more gaps beyond the original wire-protocol bug, all of the same class: relying only on require_scope(...) (an agent-capability gate that passes human identities through unconditionally) or on nothing at all, with zero check that the caller has any real relationship to the resource being written.

Fixed: - POST /repos/{repo_id}/branches/{name}/reset -- force-moves any branch to any commit with no check at all. Now requires owner-or-admin (matches the trust level of other settings-changing operations). - POST/POST /repos/{repo_id}/proposals/{id}/close|reopen -- no repo fetch, no auth check whatsoever; any authenticated identity could close or reopen any proposal in any repo, including resurrecting merged ones. Now requires owner-or-write/admin-collaborator, matching merge_proposal. - POST /repos/{repo_id}/sessions (+ /{id}/stop) -- no write-access check. Now requires owner-or-write/admin-collaborator. - POST /repos/{repo_id}/symbol-index/rebuild -- no write-access check (lower severity: derived data, but still an uncontrolled cross-repo write + compute-cost trigger against private repos). Now requires owner-or-write/admin-collaborator. - POST/DELETE /api/orgs/{org}/members/{handle} -- add_org_member and remove_org_member never checked the actor at all, only stamped them as commit author. Any authenticated identity could add itself as an admin member of any org, or remove any member from any org. Added _assert_org_write_access: owner-or-admin-member, with a bootstrap exception for a brand-new org's first member (otherwise nobody could ever become the first admin). - Collaborator-management actor-permission checks (REST collaborators.py, MCP write_tools/repos.py and write_tools/collaborators.py) didn't filter accepted_at IS NOT NULL, so a pending (not yet accepted) admin invite could exercise admin rights before accepting. Added the filter, matching the pattern already correct in musehub_repository.check_write_access.

16 new regression tests (tests/test_authz_phase3_sweep.py) prove each gap closed while confirming legitimate access still works. Full existing suite for every touched area (branch reset, proposals, sessions, collaborators, MCP write-tools, symbol intelligence -- 379 tests total) passes with zero regressions.

sha256:972eba74056d449071cb89543bfc58de94138f3826d2fd1e31491583cd8298a7 sha
+62 ~17 symbols
8 changed · 1063 in snapshot files
sha256:540ade6973bb7120f734e4a3c5146ae260a655b87f08760c92e04eb1866f6ff1 snapshot
+62
symbols added
~17
symbols modified
8
files changed
1063
files in snapshot
0
dead code introduced
Semantic Changes 79 symbols
+ docs/
+ src/
+ tests/
+ tools/
~ tests/test_authz_phase3_sweep.py .py 61 symbols added
+ _ADMIN_COLLAB variable variable _ADMIN_COLLAB L51–51
+ _ADMIN_COLLAB_CLAIMS variable variable _ADMIN_COLLAB_CLAIMS L62–62
+ _ADMIN_COLLAB_ID variable variable _ADMIN_COLLAB_ID L52–52
+ _ATTACKER variable variable _ATTACKER L47–47
+ _ATTACKER_CLAIMS variable variable _ATTACKER_CLAIMS L60–60
+ _ATTACKER_ID variable variable _ATTACKER_ID L48–48
+ _OWNER variable variable _OWNER L45–45
+ _OWNER_CLAIMS variable variable _OWNER_CLAIMS L59–59
+ _OWNER_ID variable variable _OWNER_ID L46–46
+ _WRITE_COLLAB variable variable _WRITE_COLLAB L49–49
+ _WRITE_COLLAB_CLAIMS variable variable _WRITE_COLLAB_CLAIMS L61–61
+ _WRITE_COLLAB_ID variable variable _WRITE_COLLAB_ID L50–50
+ _add_collaborator function async_function _add_collaborator L83–96
+ _claims function function _claims L55–56
+ _make_client function async_function _make_client L65–71
+ _make_repo function async_function _make_repo L74–80
+ _proposal function async_function _proposal L174–198
+ _seed_branch_with_commits function async_function _seed_branch_with_commits L106–121
+ _seed_identity function async_function _seed_identity L99–103
+ ASGITransport import import ASGITransport L29–29
+ AsyncClient import import AsyncClient L29–29
+ AsyncSession import import AsyncSession L30–30
+ MSignContext import import MSignContext L34–34
+ MusehubBranch import import MusehubBranch L39–39
+ MusehubCollaborator import import MusehubCollaborator L37–37
+ MusehubCommit import import MusehubCommit L39–39
+ MusehubCommitRef import import MusehubCommitRef L39–39
+ MusehubIdentity import import MusehubIdentity L38–38
+ MusehubRepo import import MusehubRepo L39–39
+ add_org_member import import add_org_member L41–41
+ annotations import import annotations L23–23
+ app import import app L40–40
+ compute_branch_id import import compute_branch_id L35–35
+ compute_collaborator_id import import compute_collaborator_id L35–35
+ compute_identity_id import import compute_identity_id L35–35
+ create_org import import create_org L41–41
+ create_proposal import import create_proposal L42–42
+ create_repo import import create_repo L43–43
+ datetime import import datetime L25–25
+ fake_id import import fake_id L32–32
+ get_db import import get_db L36–36
+ pytest import import pytest L27–27
+ pytest_asyncio import import pytest_asyncio L28–28
+ require_valid_token import import require_valid_token L33–33
+ timezone import import timezone L25–25
+ test_accepted_admin_collaborator_can_invite_others function async_function test_accepted_admin_collaborator_can_invite_others L401–415
+ test_add_first_org_member_bootstrap_allowed function async_function test_add_first_org_member_bootstrap_allowed L309–320
+ test_add_org_member_rejects_non_admin function async_function test_add_org_member_rejects_non_admin L324–339
+ test_admin_member_can_add_another_member function async_function test_admin_member_can_add_another_member L360–375
+ test_branch_reset_allows_owner function async_function test_branch_reset_allows_owner L157–166
+ test_branch_reset_rejects_non_collaborator function async_function test_branch_reset_rejects_non_collaborator L129–138
+ test_branch_reset_rejects_plain_write_collaborator function async_function test_branch_reset_rejects_plain_write_collaborator L142–153
+ test_close_proposal_allows_owner function async_function test_close_proposal_allows_owner L228–234
+ test_close_proposal_rejects_non_collaborator function async_function test_close_proposal_rejects_non_collaborator L202–208
+ test_create_session_allows_write_collaborator function async_function test_create_session_allows_write_collaborator L252–259
+ test_create_session_rejects_non_collaborator function async_function test_create_session_rejects_non_collaborator L242–248
+ test_pending_admin_collaborator_cannot_invite_others function async_function test_pending_admin_collaborator_cannot_invite_others L384–397
+ test_rebuild_symbol_index_rejects_non_collaborator function async_function test_rebuild_symbol_index_rejects_non_collaborator L286–301
+ test_remove_org_member_rejects_non_admin function async_function test_remove_org_member_rejects_non_admin L343–356
+ test_reopen_proposal_rejects_non_collaborator function async_function test_reopen_proposal_rejects_non_collaborator L212–224
+ test_stop_session_rejects_non_collaborator function async_function test_stop_session_rejects_non_collaborator L263–278
~ musehub/services/musehub_orgs.py .py 1 symbol added, 2 symbols modified
+ _assert_org_write_access function async_function _assert_org_write_access L108–133
Files Changed
+1 ~7
1063 in snapshot
← Older Oldest on security/131-phase3-sweep-fixes
All commits
Newer → Latest on security/131-phase3-sweep-fixes

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:972eba74056d449071cb89543bfc58de94138f3826d2fd1e31491583cd8298a7 --body "your comment"