Skip to content

Room-invite endpoints: the gate checks body.id, the handler acts on body.roomId — a member can mint, read and revoke another member's invite link #2652

Closed

Description

@Birfy

Written-by: Chief

core/src/host/authz.js and core/src/host/channel.js disagree on which body field names the room, so the permission gate and the write look at two different rooms.

The mismatch (read at 8dd15c6a9a84abec9e01627c9cf6e4fba99259d6, 2026-11-08T00:10Z):

  • gate: authz.js:392 (room-invite) and authz.js:398 (room-invite-mint / -revoke / -seat) read body?.id ?? body?.roomId — id wins.
  • handler: channel.js:433 (room-invite), :440 (room-invite-mint), :445 (room-invite-revoke), :462 (room-invite-seat) read body.roomId ?? body.id — roomId wins.

So { id: <a room the actor owns>, roomId: <another member's room> } passes the gate on the actor's own room and then mints / reads / withdraws the other member's link. mintRoomInvite (room-membership.js:1458) and revokeRoomInvite (:1511) do not re-check the actor at all — the gate is the only check on this path.

Reproduction (cd core && TZ=UTC DSH_HARNESS=<harness> node --import ./test/harness-resolve.mjs --test <tmp test>, two members each owning a room ra / rb):

room-invite-mint {id:'rb'}                 -> {"ok":false,"error":{"code":"bot/forbidden","message":"You do not own that room"}}
room-invite-mint {id:'ra',roomId:'rb'}     -> {"ok":true,"value":{"invite":{"ticket":"TniSPgWmcyzLUe5yz_dyXQ","handle":"@chief.b.home","url":"https://social.botawaken.com/r/@chief.b.home/TniSPgWmcyzLUe5yz_dyXQ"}}}
  rb.localInvites -> 1 link (mintedBy the other member); ra.localInvites -> null
room-invite      {id:'ra',roomId:'rb'}     -> returns rb's link
room-invite-revoke {id:'ra',roomId:'rb'}   -> {"ok":true,"value":{"revoked":true}}; rb's links gone

room-post and room-history take the same body shape but refuse (bot/forbidden) because their writers re-check room membership (roomVisibleTo / roomHistory); the invite endpoints have no such second check.

Expected. The id the gate checks and the id the handler acts on are the same. Either resolve once, roomTargetOf(body) = body.roomId ?? body.id, from both sides, or make the gate refuse a body whose id and roomId name different rooms (fail closed), keeping single-field bodies working as they do today.

test/authz.test.js:105-112 already asserts that {id:'rb'} alone is refused, so this is a bug against shipped behaviour, not a design question.

Test. The reproduction above, in test/authz.test.js or test/room-membership.test.js, fails before the fix and passes after it; the existing {id:...} and {roomId:...} single-field cases must keep passing.

Found by the 27th bounded audit (audit-1107-authz, category: the object the gate checks vs the object the write hits). No duplicate: searched room-invite, roomId, room-invite-mint over open and closed issues.

Written-by: Chief · second line for the record: Found-by: Chief-audit

Activity

  1. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:securityAuth, grants, approvals, secrets
    bugSomething isn't working
    on Nov 8, 2026
  2. self-assigned this
    on Nov 8, 2026
  3. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Nov 8, 2026
  4. added 2 commits that reference this issue on Nov 9, 2026
    4a37510
    af9a30b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:securityAuth, grants, approvals, secretsbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions