fix: restrict pending invitations to admins on the Team page#12
Merged
Conversation
The Team page fetched every pending invitation, including its id, and passed the list to the client TeamPanel for all authenticated users. Client component props are serialized into the page payload, and an invitation id is its acceptance token (accept-invite resolves the invite by that id). So any member could read a pending invite token from the page and accept the seat, including an admin invite. Move the query into listPendingInvitations, which returns an empty list for non-admins so a token never reaches them. Non-admins already could not act on invitations in the UI. Adds a test for both paths.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The Team page (
app/(dashboard)/team/page.tsx) fetched every pending invitation, including itsid, and passed the list to the client<TeamPanel>for any authenticated user. Only the Cancel button was role-gated, not the data.Two facts turn that into a privilege-escalation path:
idreaches the browser of anyone who can load/team(a plain member, or an account with no membership at all, since the dashboard layout only checks that a session exists).idis its acceptance token:app/accept-invite/page.tsxresolves the invite witheq(invitation.id, token), and the accept URL is?token=${id}.So a member could read a pending invite's token from the page source and accept the seat themselves, including an invite issued for the
adminrole.The fix
Move the invitation query into
listPendingInvitations(db, orgId, actorIsAdmin)inlib/team.ts, which returns an empty list for non-admins so a token never enters the payload. Non-admins already could not act on invitations in the UI, so the visible behavior for them is just an empty "pending invitations" section.Adds
lib/team.test.tscovering both paths (non-admin gets nothing and the token string is absent; admin gets the invitation) plus the pending-status filter.Verification
npm run typecheckcleannpm run lintcleannpm test245/245 (was 242, +3 new)npm run buildexit 0