Lorem, ipsum dolor sit amet consectetur adipisicing elit. Qui, itaque voluptate ipsa non enim amet ducimus voluptatibus deserunt nam esse!
Auditing Multi-Tenant Workspace Invitations for Access Control Bugs

Auditing Multi-Tenant Workspace Invitations for Access Control Bugs

pr0h0
access-controlmulti-tenantsecurity-auditworkspace-invitations
AI Usage (89%)

Why workspace invitations are a good place to look

Workspace invites sit right on the edge between “outside user” and “trusted member,” which makes them easy to mishandle.

I usually start here because invite flows often combine three risky pieces:

  • a token that looks opaque but is really just an identifier
  • a tenant or workspace selector in the client
  • a role value that changes permissions after acceptance

If any one of those gets too much trust, a low-privilege user can land in the wrong workspace, with the wrong role, or both. The bug is rarely in the invite email itself. It usually shows up in the acceptance endpoint, the membership update, or the post-acceptance permission sync.

💪

Test invites as if the browser were hostile. If the client can choose a workspace, it can usually lie about it.

The trust boundaries that usually get missed

Invite token handling

The token should identify a specific invitation, not behave like a reusable pass. In audits, I look for token formats that leak structure or encode claims the server never re-checks.

Questions to answer:

  • Is the token one-time use?
  • Does it expire?
  • Does it bind to a specific workspace and email address?
  • Is it invalidated after acceptance or rejection?

If the token can be replayed, forwarded, or accepted under a different account, the boundary is already broken.

Workspace membership checks

Acceptance is not enough. The server should verify that the target account is allowed to join that workspace at the moment of acceptance.

A common mistake is to trust the invite token and skip a final membership check because “the link came from email.” That assumption is weak. Email is not authorization.

Role assignment and default permissions

Role assignment is where small mistakes turn into real impact. If the invite says admin, the server should decide whether that role is allowed for the inviter, the workspace policy, and the target account.

I have seen apps that:

  • accept any role sent by the client
  • default to the highest local role on missing fields
  • let a user join as a member but keep hidden admin privileges from an earlier invite state

A practical audit flow in the browser and API

Reproduce the invite as a low-privilege user

Start with two accounts in different tenants. One should be the inviter, the other a fresh or free account.

Reproduce the full flow:

  1. create invite from workspace A
  2. open the invite as the target account
  3. accept it
  4. verify the workspace list, role, and available actions

Do not stop at “the page loaded.” You want the server-side state.

Inspect the network requests and server responses

Open DevTools and watch the acceptance request closely. I care about:

  • request body fields like workspaceId, tenantId, inviteId, role
  • response codes on success and failure
  • whether the server echoes sensitive fields back
  • whether the client makes a second request to fetch membership or permissions

A lot of bugs show up in the gap between the first acceptance response and the later “load workspace context” request.

// invite-audit.js
const fetchLog = [];
const originalFetch = window.fetch;

window.fetch = async (...args) => {
  const [url, init] = args;
  fetchLog.push({
    url,
    method: init?.method || "GET",
    body: init?.body || null
  });
  return originalFetch(...args);
};

That kind of logging helps when the UI hides what the browser is actually sending.

Change tenant IDs, invite IDs, and role values safely

Use harmless variations against your own test accounts:

  • replace the workspace ID with another workspace you control
  • swap the invite ID with an expired or already-used one
  • change the role from member to admin and see whether the backend rejects it

The point is not to break things blindly. The point is to see which checks happen on the server and which ones only exist in the interface.

⚠️

Do not test with real customer workspaces or production invites unless you have explicit permission and a safe test plan.

Common bug patterns in multi-tenant invite flows

IDOR on invite acceptance

The classic version is simple: the acceptance endpoint takes an inviteId, and the server does not verify that the current user is the intended recipient.

If you can accept someone else's invite, the app has an authorization problem. If you can enumerate invite IDs and discover valid workspaces, you also have an information leak.

Cross-workspace membership drift

This shows up when membership records are copied or cached incorrectly.

Example pattern:

  • user accepts invite into workspace A
  • backend creates membership in A
  • frontend switches context and still shows permissions from workspace B
  • later API calls are authorized using stale client state

The bug becomes dangerous when the client and server disagree about the active tenant. The UI may look safe while the API still honors the wrong membership.

UI-only restrictions that never reach the backend

If the invite modal disables admin in the browser but the API accepts it anyway, the restriction is cosmetic.

That is not a harmless bug. It means any user who can tamper with the request can ask for a higher role than the UI intended to allow.

What to log, verify, and report

Evidence that proves tenant boundaries were bypassed

Good evidence is boring and precise:

  • request and response for the invite acceptance
  • account used
  • workspace before and after acceptance
  • role assigned
  • any changed permissions that were not supposed to be granted

A clean report should show that the backend accepted an action across a tenant boundary, not just that the UI behaved strangely.

Impact statements that stay concrete

Avoid broad claims like “full account takeover” unless you can show it.

Better impact statements:

  • a free user could join a private workspace they were never invited to
  • a member could accept an invite as admin and gain access to administrative actions
  • an expired invite could be replayed to create duplicate memberships

Defensive checks that close the gap

Server-side authorization on every invite action

Every invite endpoint needs its own authorization check:

  • create invite
  • view invite
  • accept invite
  • resend invite
  • revoke invite

Do not assume that the first step guarantees the rest.

Token scope, expiry, and one-time use

Tokens should be scoped tightly:

  • one workspace
  • one recipient
  • short expiry
  • single use
  • invalidation after acceptance or revocation

If the invite token can be reused, the rest of the system has to be perfect forever. That is a bad bet.

Post-acceptance permission reconciliation

After acceptance, the server should recompute effective permissions from current workspace policy, not from whatever the client requested earlier.

That means:

  • reload membership from the database
  • ignore stale client role values
  • enforce tenant membership before returning workspace data
  • log mismatches between invite intent and granted access

Conclusion

Workspace invitations look harmless, but they are one of the easiest places to smuggle access control bugs into a multi-tenant app. The browser can lie, the token can be replayed, and the UI can drift from the backend.

If you test only the happy path, you will miss the real problem. If you verify token scope, server-side membership checks, and post-acceptance role reconciliation, you will catch the bugs that actually matter.

Share this post

More posts

Comments