Severity: Medium
CVSS: 6.3 (CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:N)
Endpoint: POST https://api.vibekanban.com/v1/invitations/{token}/accept (also GET /v1/invitations/{token})
Affected: Vibe Kanban Cloud / Remote, server version 0.1.27
Title: Organization invitation can be redeemed by any account, not just the invited email
(unauthorized org join with the invited role, including Admin)
Summary:
When an organization invitation is accepted, the server binds membership to whichever user is
currently authenticated and grants them the role stored on the invitation — it never verifies
that the authenticated user's email matches the address the invitation was sent to. Any
authenticated Vibe Kanban user who obtains a pending invitation token can therefore join the
target organization, with the role chosen by the inviter (which may be Admin). The token is
distributed only inside the invitation email, but invitation links are routinely
forwarded/mis-sent/leaked (mailing lists, helpdesk tickets, proxy and referer logs, screenshots),
and the standard control against exactly this — binding redemption to the invited email — is
absent. Additionally, the invitation lookup endpoint is unauthenticated and discloses the target
organization and the granted role for any token before it is accepted.
Root cause (open-source, github.com/BloopAI/vibe-kanban, matches live v0.1.27):
- crates/remote/src/db/invitations.rs ~193 accept_invitation(token, user_id): selects the
invitation by token AND status='pending', then checks only: not a personal org, not expired,
and the caller is not already a member. It then calls
add_member(org, user_id, invitation.role) for the authenticated user_id. The invitation row
has an email column, but it is never compared to the accepting user's email.
- crates/remote/src/routes/organization_members.rs ~274 accept_invitation handler: passes
ctx.user.id straight through; no email check.
- crates/remote/src/routes/organization_members.rs ~205 get_invitation: registered in the PUBLIC
(unauthenticated) router and returns organization_slug, organization_name, role and expires_at
for any token.
Steps / PoC:
Preconditions: an org admin has invited some address (e.g. alice@victim-org.com) as a member or
Admin (this creates a pending invitation with a token sent in the email). The attacker has any
ordinary Vibe Kanban account and has obtained the token (e.g. a forwarded invite link).
-
(Optional, unauthenticated) Confirm the token and see what it grants, before accepting:
curl https://api.vibekanban.com/v1/invitations/
-> 200 {"id":"...","organization_slug":"victim-org","organization_name":"Victim Org",
"role":"admin","expires_at":"..."}
-
As the attacker (any account, email = attacker@example.com), accept it:
curl -X POST https://api.vibekanban.com/v1/invitations//accept
-H "Authorization: Bearer <attacker_access_token>"
-> 200 {"organization_id":"","organization_slug":"victim-org","role":"admin"}
The attacker is now a member (here, Admin) of the victim organization despite the invitation
having been addressed to alice@victim-org.com. With Admin they can read all projects/issues,
manage members, manage the org's GitHub App installation, and access billing settings.
Verification status:
- The public GET /v1/invitations/{token} and the POST .../accept routes are live and reachable on
api.vibekanban.com (a non-existent token returns 404; the routes are registered as shown).
- The missing email-binding is a deterministic authorization omission in the deployed open-source
code (paths cited; live x-server-version = 0.1.27 matches the repo). This logic is always active
(not behind any feature flag). End-to-end execution against a real victim org was not performed
because creating a separate authenticated cloud account requires interactive Google/GitHub OAuth
(the cloud has no programmatic signup), but the code path is unambiguous and unconditional.
Impact:
Unauthorized access to an organization's data and (when the invite role is Admin) full
administrative control, granted to an account that was never invited. This breaks the tenant
trust boundary whenever an invitation token reaches anyone other than the intended recipient —
a common real-world occurrence — and the unauthenticated invitation-lookup endpoint makes a
leaked token immediately actionable by revealing the org and role it confers.
Fix:
In accept_invitation, load the accepting user's email and require a case-insensitive match against
invitation.email before calling add_member; reject with 403 otherwise. Consider requiring the
invited account to exist/verify that email at the identity layer. Restrict GET /v1/invitations/
{token} to disclose the minimum necessary, and rate-limit it. Expire/rotate tokens on first view
where feasible.
Severity: Medium
CVSS: 6.3 (CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:N)
Endpoint: POST https://api.vibekanban.com/v1/invitations/{token}/accept (also GET /v1/invitations/{token})
Affected: Vibe Kanban Cloud / Remote, server version 0.1.27
Title: Organization invitation can be redeemed by any account, not just the invited email
(unauthorized org join with the invited role, including Admin)
Summary:
When an organization invitation is accepted, the server binds membership to whichever user is
currently authenticated and grants them the role stored on the invitation — it never verifies
that the authenticated user's email matches the address the invitation was sent to. Any
authenticated Vibe Kanban user who obtains a pending invitation token can therefore join the
target organization, with the role chosen by the inviter (which may be Admin). The token is
distributed only inside the invitation email, but invitation links are routinely
forwarded/mis-sent/leaked (mailing lists, helpdesk tickets, proxy and referer logs, screenshots),
and the standard control against exactly this — binding redemption to the invited email — is
absent. Additionally, the invitation lookup endpoint is unauthenticated and discloses the target
organization and the granted role for any token before it is accepted.
Root cause (open-source, github.com/BloopAI/vibe-kanban, matches live v0.1.27):
invitation by
token AND status='pending', then checks only: not a personal org, not expired,and the caller is not already a member. It then calls
add_member(org, user_id, invitation.role) for the authenticated user_id. The invitation row
has an
emailcolumn, but it is never compared to the accepting user's email.ctx.user.id straight through; no email check.
(unauthenticated) router and returns organization_slug, organization_name, role and expires_at
for any token.
Steps / PoC:
Preconditions: an org admin has invited some address (e.g. alice@victim-org.com) as a member or
Admin (this creates a pending invitation with a token sent in the email). The attacker has any
ordinary Vibe Kanban account and has obtained the token (e.g. a forwarded invite link).
(Optional, unauthenticated) Confirm the token and see what it grants, before accepting:
curl https://api.vibekanban.com/v1/invitations/
-> 200 {"id":"...","organization_slug":"victim-org","organization_name":"Victim Org",
"role":"admin","expires_at":"..."}
As the attacker (any account, email = attacker@example.com), accept it:
curl -X POST https://api.vibekanban.com/v1/invitations//accept
-H "Authorization: Bearer <attacker_access_token>"
-> 200 {"organization_id":"","organization_slug":"victim-org","role":"admin"}
The attacker is now a member (here, Admin) of the victim organization despite the invitation
having been addressed to alice@victim-org.com. With Admin they can read all projects/issues,
manage members, manage the org's GitHub App installation, and access billing settings.
Verification status:
api.vibekanban.com (a non-existent token returns 404; the routes are registered as shown).
code (paths cited; live x-server-version = 0.1.27 matches the repo). This logic is always active
(not behind any feature flag). End-to-end execution against a real victim org was not performed
because creating a separate authenticated cloud account requires interactive Google/GitHub OAuth
(the cloud has no programmatic signup), but the code path is unambiguous and unconditional.
Impact:
Unauthorized access to an organization's data and (when the invite role is Admin) full
administrative control, granted to an account that was never invited. This breaks the tenant
trust boundary whenever an invitation token reaches anyone other than the intended recipient —
a common real-world occurrence — and the unauthenticated invitation-lookup endpoint makes a
leaked token immediately actionable by revealing the org and role it confers.
Fix:
In accept_invitation, load the accepting user's email and require a case-insensitive match against
invitation.email before calling add_member; reject with 403 otherwise. Consider requiring the
invited account to exist/verify that email at the identity layer. Restrict GET /v1/invitations/
{token} to disclose the minimum necessary, and rate-limit it. Expire/rotate tokens on first view
where feasible.