Problem
apps/web/src/app/(auth)/accept-invite/page.tsx — acceptInviteAction takes orgSlug from the query string and adds the currently-authenticated Supabase user to that org, with no verification that:
- The authenticated user's email matches the invited email
- The invite has not already been accepted (no consumed/single-use guard)
- The
orgSlug in the URL corresponds to the org the invite was issued for
Result: any user with an active Supabase session can navigate to /accept-invite?org=target-org-slug and join an arbitrary org as a viewer. Additionally, if Supabase magic-link tokens are not single-use (they are not by default), two people can click the same invite link and both gain membership.
Fix
// In acceptInviteAction:
const { data: invite } = await supabase
.from('org_invites')
.select('email, org_id, accepted_at')
.eq('token', inviteToken)
.single();
if (!invite || invite.accepted_at) return { error: 'Invalid or expired invite' };
if (invite.email !== session.user.email) return { error: 'Email mismatch' };
// Mark consumed
await supabase.from('org_invites').update({ accepted_at: new Date() }).eq('token', inviteToken);
Requires an org_invites table with token + consumed tracking.
Severity
HIGH — unauthorized org membership; any user with the invite URL slug can join.
Problem
apps/web/src/app/(auth)/accept-invite/page.tsx—acceptInviteActiontakesorgSlugfrom the query string and adds the currently-authenticated Supabase user to that org, with no verification that:orgSlugin the URL corresponds to the org the invite was issued forResult: any user with an active Supabase session can navigate to
/accept-invite?org=target-org-slugand join an arbitrary org as a viewer. Additionally, if Supabase magic-link tokens are not single-use (they are not by default), two people can click the same invite link and both gain membership.Fix
Requires an
org_invitestable with token + consumed tracking.Severity
HIGH — unauthorized org membership; any user with the invite URL slug can join.