# Vikunja: Link-share principal-type confusion enables cross-account team removal, bot takeover, and roster disclosure

Vikunja link-share principals return raw positive share IDs, colliding with users.id at three unguarded permission checks, enabling cross-account team removal, bot deletion, and roster disclosure.

- Payout: Research
- Severity: high
- Program: Vikunja
- Found by: manus-use
- Disclosed: 2026-10-09
- CVE: CVE-2026-76216
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-32r8-5843-4qw2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-32r8-5843-4qw2
- Tags: area:web domain:web impact:data-leak impact:dos impact:integrity kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:idor

**Target:** Go REST API permission checks (web.Auth interface, team/bot endpoints)
**Root cause:** web.Auth is a one-method interface (GetID() int64) satisfied by both *user.User and *models.LinkSharing; LinkSharing.GetID() returns the raw positive share.ID in the same autoincrement space as users.id. Three permission sinks (TeamMember.CanDelete self-removal fast path, BotUser.isOwner, Team.CanRead) compare this raw id without the a.(*LinkSharing) guard their sibling methods use.
**Steps:**
1. Obtain a link-share JWT via `POST /api/v1/shares/{hash}/auth` whose share id N equals a victim's users.id (opportunistic or by walking autoincrement).
2. Send `DELETE /api/v1/teams/{T}/members/{usernameOfUserN}` with the link-share bearer; CanDelete's `u.ID == a.GetID()` fast path returns true before IsAdmin's LinkSharing guard.
3. Send `GET /api/v1/user/bots` to enumerate victim bots via `bot_owner_id = a.GetID()`, then `DELETE /api/v1/user/bots/{botId}` to permanently delete them through isOwner.
4. Send `GET /api/v1/teams/{T}` to read roster and metadata via CanRead's `user_id = a.GetID()` membership match.
**How found:** Code review of the web.Auth interface: noticed LinkSharing.GetID() returns raw positive share.ID while a safe negated getUserID() exists but is unused; then audited permission methods for missing a.(*LinkSharing) guards, finding three sinks where sibling methods (IsAdmin, CanCreate) had the guard but these paths did not.
**Exploitation:** Attacker holds a link-share JWT (from any public share or self-registered share on own project) with id N colliding with victim users.id. Using that bearer against /teams and /user routes, they remove the victim from teams (revoking inherited project permissions), enumerate and delete the victim's bot users, and read team rosters. Collision is opportunistic since share ids are autoincrement; targeted attacks walk the counter toward low-numbered/admin accounts.
**Escalation:** Chained three separate sinks from one principal-confusion primitive: integrity (team removal), availability/integrity (bot deletion), confidentiality (roster disclosure).
**Insight:** When an auth interface is satisfied by multiple principal types, grep every GetID()/id comparison for the type guard — the bug hides where a fast-path early return (self-check) precedes the only method that rejects the wrong principal type, and where a guard exists on Create but not Read/Update/Delete siblings.

