Invite Flow Privilege Escalation in Multi-Tenant Applications
Invitations cross a dangerous boundary. An administrator chooses a tenant and role, the system sends a bearer token through email, and an unauthenticated recipient turns that token into membership. If any part of that handoff trusts the browser, a basic invite can become an owner seat.
I like invite testing because the proof stays tidy: one organization, a few accounts you own, and no need to touch customer data.
Capture every stage
Create an organization with owner account A. Invite account B as the lowest role. Record:
- The request that creates the invitation
- The email link and token format
- The page that previews tenant and role
- The acceptance request
- The membership object returned afterward
Look for role, organizationId, workspaceId, inviterId, and email in both creation and acceptance. Values shown by the front end are not necessarily trusted, but they are excellent mutation targets.
Change B's acceptance body from role: member to role: owner. Remove the field, duplicate it, alter case, or send an internal role name found in JavaScript such as ORG_ADMIN. The server should derive role and tenant solely from the stored invitation, never from acceptance input.
Cross-tenant swaps
Make a second organization under account C. Create one invite in each tenant and swap token-adjacent identifiers. Can a token issued for tenant one be accepted into tenant two? Does changing only organizationId move the membership while preserving the invited role?
Object-level checks can fail on the invite creation side too. As a low-privilege member, replay the owner's creation request and change the target role. A hidden button is not authorization. If the API lets a member invite an owner, the escalation is direct.
Replay, expiry, and identity binding
Accept B's invite, remove B from the organization, then reuse the same link. A reusable token may let a removed member walk back in. Request that the owner revoke a pending invite and test it once more. Revocation that only hides the row in the UI is not revocation.
Now accept while logged in as a different owned account. Some products intentionally allow this after email verification; others promise that only the invited address can join. Compare behavior with the product's model. Case changes, plus-addresses, Unicode normalization, and an email changed after invite creation can expose binding mistakes.
Expiry should be enforced server-side. Changing a timestamp in the browser is a red flag, though signed tokens may safely carry their own expiry. If an old invite survives a role downgrade of the inviter or deletion of the workspace, stale authority has outlived its source.
Make impact visible
After escalation, call one owner-only endpoint that changes a harmless setting in your test tenant, then restore it. Showing the role label alone can be misleading if backend authorization still blocks owner actions.
Your report should tell a short story: owner creates member invite, recipient changes one field or replays one token, server grants owner capability. Include before-and-after role responses and the protected action.
Fix by storing tenant, recipient, role, expiry, and one-time status server-side; make acceptance consume that record atomically; authorize invite creation by requested role; invalidate tokens on revocation and membership changes; and require fresh confirmation for owner-level invitations. Invite links are capabilities. Scope them like credentials.