Skip to content

fix: restrict pending invitations to admins on the Team page#12

Merged
TrueLineCollective merged 1 commit into
mainfrom
fix/team-invite-token-exposure
Jul 2, 2026
Merged

fix: restrict pending invitations to admins on the Team page#12
TrueLineCollective merged 1 commit into
mainfrom
fix/team-invite-token-exposure

Conversation

@TrueLineCollective

Copy link
Copy Markdown
Owner

The bug

The Team page (app/(dashboard)/team/page.tsx) fetched every pending invitation, including its id, 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:

  1. Client component props are serialized into the page's payload, so the invitation id reaches 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).
  2. An invitation's id is its acceptance token: app/accept-invite/page.tsx resolves the invite with eq(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 admin role.

The fix

Move the invitation query into listPendingInvitations(db, orgId, actorIsAdmin) in lib/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.ts covering both paths (non-admin gets nothing and the token string is absent; admin gets the invitation) plus the pending-status filter.

Verification

  • npm run typecheck clean
  • npm run lint clean
  • npm test 245/245 (was 242, +3 new)
  • npm run build exit 0

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.
@TrueLineCollective TrueLineCollective merged commit c91b796 into main Jul 2, 2026
6 checks passed
@TrueLineCollective TrueLineCollective deleted the fix/team-invite-token-exposure branch July 2, 2026 02:01
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