[Bug] Org member can self-promote to admin and bypass role boundaries
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 407
- Forks
- 57
- Avg merge
- 5h 6m
- Merged PRs (30d)
- 880
Description
Summary
A user with member role in an org can call ORGANIZATION_MEMBER_UPDATE_ROLE against their own member row and promote themselves to admin. This breaks the segregation of duties expected for non-tech members (e.g. CEO, compliance, support) operating an MCP gateway. Related closed issues #327 (Segregation of access) and #853 ([Permissions] authorization for any integration tool) suggest this is supposed to work, but the surface area shipped today is permissive.
Reproduce
- Log in as org
owner, invite a second account with rolemember. - Sign in as the new member. Confirm the role on
membertable ismember. - From the member's session, call the management MCP self endpoint:
curl -X POST 'http://<studio>/api/<org>/mcp/self' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Origin: http://<studio>' -b <member-session-cookie> \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"ORGANIZATION_MEMBER_UPDATE_ROLE",
"arguments":{"memberId":"<their-own-member-id>","role":["admin"]}}}'
- Observed: the DB row flips to
admin. The member is now an admin. - Expected: 403 Access denied —
ORGANIZATION_MEMBER_UPDATE_ROLErequires admin/owner; even with admin, mutating one's own row should be guarded.
What I think is going on
apps/mesh/src/auth/index.tsregistersuser/admin/ownerroles as{ self: ["*"], ...adminAc.statements }— wildcard on every MCP tool. Better Auth's organization plugin default for new members is the role string"member", which isn't in theroles: { user, admin, owner }map, so member rows fall through to undefined or some permissive default.- Even when patched locally so
rolesincludes a narrowly-scopedmember(read-only on connections +memberAc.statementsfrom the org plugin), the call still succeeded. Smells likectx.access.checkhas an alternate code path, or Better Auth caches role defs acrossbun --hotreloads. AccessControl.checkResourceatapps/mesh/src/core/access-control.tsearly-returnstruefor roles literally namedadminorowner, then delegates toboundAuth.hasPermission. ThepermissionToCheckis keyed byconnectionId(default"self") and the action is the tool name. Worth instrumenting to log whatboundAuth.hasPermissionactually returns for a member callingORGANIZATION_MEMBER_UPDATE_ROLE.
Desired outcome (proposed scope)
memberrole gets read-only access to org metadata and connections, plus write access ONLY to their own per-user OAuth tokens. No mutation of org settings, members, invitations, connection definitions, virtual MCPs.- Built-in roles
owner/admin/memberare first-class and consistent between static config (apps/mesh/src/auth/index.ts) and the Dynamic Access Control surface (creating a "custom user" from a built-in role currently 400s withTHAT_ROLE_NAME_IS_ALREADY_TAKEN). - Integration test that asserts a
member-role caller cannot call any tool in theORGANIZATION_*/COLLECTION_*mutation families, norMEMBER_UPDATE_ROLEagainst themselves.
Context
Surfaced while building per-user OAuth on downstream MCPs (CEO uses Notion as themselves, compliance uses internal tools as themselves) over an organization-wide unified MCP gateway. The feature works end-to-end, but role isolation is the missing safety floor for non-tech members.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with apps/mesh/src/auth/index.ts and apps/mesh/src/core/access-control.ts, tracing ctx.access.check and boundAuth.hasPermission for a member calling ORGANIZATION_MEMBER_UPDATE_ROLE. Define the built-in member permissions and verify the static and Dynamic Access Control role behavior. Add the integration coverage described for ORGANIZATION_/COLLECTION_ mutations and self-role changes, then confirm member callers receive 403 responses.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, typescript
- Domain
- authorization, backend-api-design, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100