Skip to content

fix(server): deny restricted owners from escalating collection access (BUG-1925)#815

Merged
xarmian merged 1 commit into
mainfrom
fix/bug-1925-restricted-owner-escalation
Jul 4, 2026
Merged

fix(server): deny restricted owners from escalating collection access (BUG-1925)#815
xarmian merged 1 commit into
mainfrom
fix/bug-1925-restricted-owner-escalation

Conversation

@xarmian

@xarmian xarmian commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

fix(server): deny restricted owners from escalating collection access (BUG-1925)

handleSetMemberCollectionAccess gated only on requireRole("owner") (workspace role) — but member_collection_access has no role exclusion (BUG-1920), so a workspace-role "owner" can independently be restricted (collection_access="specific"). Such an owner could PATCH their own member record to mode="all" — instantly unrestricted — or into a hidden collection, defeating the entire BUG-1917/1918/1920/1921 visibility family and the BUG-1922 export gate in one authenticated request. This is the self-escalation hole those fixes' threat model assumed was closed.

Per Dave: restricted-owner is a real security boundary → apply the minimal guard.

Change

New requireCallerCanSetCollectionAccess, called before the store write:

  • Unrestricted caller (visibleCollectionIDs nil — mode "all"/admin) → no-op, full power unchanged.
  • Restricted caller + mode="all" (any target, self or another member) → 403 — can't grant unrestricted access it doesn't hold.
  • Restricted caller + mode="specific" → every requested collection ID must pass isCollectionVisible against the caller's set → 404 per hidden ID (matches the family's hidden-resource convention; doesn't confirm existence).

Load-bearing narrowing: the check-set is narrowed from nav-lenient visibleCollectionIDs to guestResourceFilter's strict fullCollIDs whenever the caller holds item-level grants — mirroring requireCollectionFullyVisible (BUG-1920). Without it, a restricted caller with only an item-level grant into a hidden collection could launder it into collection-wide member_collection_access. Verified load-bearing: stripping the narrowing makes the item-grant test fail (200 instead of 404).

Review

  • 2 codex rounds: round 1 general CLEAN; round 2 confirmed fullCollIDs ⊆ visibleCollectionIDs (strict subset, excludes item-grant-only) and no item-grant-only bypass. Remaining edges are closed by construction: mode-casing is pre-validated to exact "all"/"specific" before the guard; cross-member and empty-list are covered by tests; nonexistent/duplicate IDs take the same isCollectionVisible→404 path as hidden IDs.

Test plan

  • go test ./... (SQLite) — pass, 6 new tests
  • make test-pg (Postgres, isolated container) — pass
  • golangci-lint run ./... — 0 issues
  • New tests: self→all 403; self specific w/ hidden ID 404; cross-member hidden ID 404; within-scope + empty list 200; item-grant-only → 404 (pins narrowing); unrestricted owner unaffected — all bearer + session, all assert store unchanged on deny

Refs: BUG-1925 (closes), BUG-1920 (narrowing precedent), BUG-1922 (export gate this protects), BUG-1917/1918/1921 (family).

https://claude.ai/code/session_01CL1pBjNpPUX6SWkuAuYXHS

… (BUG-1925)

handleSetMemberCollectionAccess gated only on requireRole(r, "owner"), with
no validation of the caller's own visibility. member_collection_access has
no role exclusion (BUG-1920), so a workspace-role owner independently
restricted via collection_access="specific" could PATCH themselves (or any
other member) to mode="all", or grant a collection outside their own
visible set — defeating the entire BUG-1917/1918/1920/1921 visibility
family plus the BUG-1922 export gate in one authenticated request.

requireCallerCanSetCollectionAccess now denies a restricted caller from
setting mode="all" for any target, and validates every requested
collection ID against the caller's own visible set, narrowed to
guestResourceFilter's fullCollIDs (mirroring requireCollectionFullyVisible,
BUG-1920 codex R2) so an item-level grant can't be escalated into
collection-wide access. Unrestricted callers are unaffected.
@xarmian
xarmian merged commit 7eff417 into main Jul 4, 2026
4 checks passed
@xarmian
xarmian deleted the fix/bug-1925-restricted-owner-escalation branch July 4, 2026 18:39
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