high · 8.1 Patched

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

CVE-2026-68581 · GHSA-vvcv-vpph-h844 · GO-2026-6753 · published 9 Oct 2026

How serious is it

  • Severity: high, CVSS score 8.1 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-68581?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • code.vikunja.io/api (Go) >= 0.22.0, <= 2.3.0

How to check

  1. Run go list -m code.vikunja.io/api in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade code.vikunja.io/api to 2.4.0 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

Executive Summary Vikunja accepts both regular-user JSON Web Tokens (JWTs) and link-share JWTs through its generic authenticated API route group. A link-share JWT resolves to a `models.LinkSharing` principal whose `GetID()` method returns the numeric `link_shares.id`. The API-token management model then treats that generic numeric value as a `users.id` when it creates, lists, and deletes API tokens. Because `users.id` and `link_shares.id` are independent positive sequences, a link share whose ID equals another user's ID is treated as that user by `/api/v1/tokens`. An ordinary authenticated attacker can obtain a target's numeric user ID through authenticated user search and create link shares on an attacker-writable project until the link-share sequence reaches that value. The colliding link-share principal can then list the target's API-token metadata, issue a new API token owned by the target, and delete target-owned API tokens. The newly issued token operates with attacker-selected valid API scopes under the target user's existing permissions. I reviewed the vulnerable `v2.3.0` source, the introducing commit, the stable tag history, and the inspected main revision directly. I also reviewed the recorded local HTTP and native-router validation results. Dynamic testing used only a local `v2.3.0` Docker deployment with PostgreSQL and synthetic data; I did not test any public, hosted, or otherwise external instance. Historical tags were checked from source rather than exercised dynamically. | Field | Value | | --- | --- | | Product | Vikunja API | | Status | Validated; disclosure-ready | | Confirmed affected stable versions | `v0.22.0` through `v2.3.0`, inclusive | | Nearest confirmed unaffected stable version | `v0.21.0` | | Introducing commit | `e6b25bd57b537ef9a72b5acdadf446ca5ef77bfa` | | Latest inspected affected main revision | `95b7e673fb5ee407498fa4b13e8b4c57847a4a0b` | | Fixed version | Unknown; no verified fix commit or release was identified | | Severity | High, CVSS v3.1 score 8.1 | | Vector | `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` | | Weakness | Primary: CWE-863 (Incorrect Authorization); secondary: CWE-639 | The stable range is source-based. The issue first became reachable when commit `e6b25bd57b...` added the authenticated API-token CRUD routes while retaining the generic `web.Auth.GetID()` ownership model. `v0.21.0` does not contain the API-token model or these routes. All containing stable tags from `v0.22.0` through `v2.3.0` retain the vulnerable source-to-sink tuple. Downstream forks and vendor-specific builds were not assessed. ## Background ### Two authenticated principal types share one numeric interface Vikunja's authenticated route group is protected by a common token middleware. That middleware can establish a regular user, an API-token owner, or a link share as the request's `web.Auth` principal. The `/api/v1/tokens` handlers are registered on this generic group in `pkg/routes/routes.go`: ```go a.Use(SetupTokenMiddleware()) // API Tokens apiTokenProvider := &handler.WebHandler{ EmptyStruct: func() handler.CObject { return &models.APIToken{} }, } a.GET("/tokens", apiTokenProvider.ReadAllWeb) a.PUT("/tokens", apiTokenProvider.CreateWeb) a.DELETE("/tokens/:token", apiTokenProvider.DeleteWeb) ``` The security-sensitive detail is that authentication answers *which object is the principal*, while `web.Auth.GetID()` exposes only an untyped integer. A regular user's ID belongs to the `users` namespace. A link-share ID belongs to the separately allocated `link_shares` namespace. Equality between those integers does not imply that the principals represent the same actor. ### Link-share authentication preserves the share-row ID `pkg/modules/auth/auth.go` creates a link-share JWT with the share's numeric ID and later resolves that token to a `models.LinkSharing` object: ```go claims["type"] = AuthTypeLinkShare claims["id"] = share.ID claims["hash"] = share.Hash claims["project_id"] = share.ProjectID // ... if typ == AuthTypeLinkShare && config.ServiceEnableLinkSharing.GetBool() { s := db.NewSession() defer s.Close() return models.GetLinkShareFromClaims(s, claims) } ``` The `LinkSharing` model declares its own positive auto-incrementing key and returns it unchanged through the generic interface in `pkg/models/link_sharing.go`: ```go type LinkSharing struct { ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"share"` // ... } func (share *LinkSharing) GetID() int64 { return share.ID } ``` Link shares are expected to act within the shared project's permission boundary. API-token management is instead a user-account credential boundary: tokens are owned by users and can authorize future requests as those users. The normal invariant should therefore be that only a positively identified regular-user principal may enter API-token management, and its user ID must be used as the owner key. ### Attacker capabilities The validated attacker is an ordinary authenticated user, not an administrator. The attacker needs a project they own or can write to so that they can create ordinary link shares. Authenticated user search exposes the matching user's numeric `id` while blanking email addresses in `pkg/routes/api/v1/user_list.go`: ```go users, err := user.ListUsers(s, search, currentUser, nil) // ... for in := range users { users[in].Email = "" } return c.JSON(http.StatusOK, users) ``` That response makes the collision target directly discoverable in the tested default flow. An operator's registration policy or user-search restrictions may alter practical target selection, but they do not restore the missing principal-type check at the API-token boundary. ## Vulnerability Details ### Source-to-sink walkthrough We first authenticate an ordinary attacker account and learn a candidate `<TARGET_USER_ID>` through `GET /api/v1/users`. We then create link shares on an attacker-writable project. `LinkSharing.Create` resets the caller-su…

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database, OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON