You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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.
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
fix(core): read the round-progress room through the same resolver as …
fix(core): resolve the room-invite target once so the gate and the ha…
Release v0.2.27 (ThinkFlowLab#2701)