Vikunja: Link-share token reads any tenant's kanban buckets and enumerates usernames/IDs instance-wide (BOLA)
CVE-2026-68582 · GHSA-rj9j-8772-4h6c · GO-2026-6749 · published 9 Oct 2026
How serious is it
- Severity: critical, CVSS score 9.3 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-68582?
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.24.0, <= 2.3.0
How to check
- Run
go list -m code.vikunja.io/apiin your project to see the installed version, including copies pulled in by other packages. - 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.
- Upgrade
code.vikunja.io/apito 2.4.0 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
The task-collection endpoint `GET /api/v1/projects/{project}/views/{view}/tasks` loads the requested project view straight from the URL path without verifying the caller is authorized for it. For a **link-share token**, the task query is correctly pinned to the share's own project, but the *view* is taken from the attacker-controlled path and never re-validated against the share. A holder of a share link to **any** project can therefore point the endpoint at any other tenant's kanban view and receive that view's bucket records — bucket titles plus the full `created_by` user object (username, name, id) — for every view in the instance. The same missing pre-authorization view load also yields a project/view-ID existence oracle (404 vs. non-404) usable by link shares and ordinary authenticated users alike.
## Details
Root cause is in `TaskCollection.ReadAll` (`pkg/models/task_collection.go`).
1. The view is resolved from the URL params before any permission check:
```go
// pkg/models/task_collection.go (ReadAll)
if tf.ProjectViewID != 0 {
view, err = GetProjectViewByIDAndProject(s, tf.ProjectViewID, tf.ProjectID) // (URL params)
...
}
```
`GetProjectViewByIDAndProject` (`pkg/models/project_view.go`) filters only on `WHERE id = ? AND project_id = ?` — it performs no authorization. A valid `(project, view)` pair returns the view; an invalid pair returns `ErrProjectViewDoesNotExist` (HTTP 404).
2. For a link-share caller, the code pins the *task* scope to the token's own project but reuses the attacker-supplied `view`:
```go
shareAuth, is := a.(*LinkSharing)
if is {
project, err := GetProjectSimpleByID(s, shareAuth.ProjectID) // token's project
...
return getTaskOrTasksInBuckets(s, a, []*Project{project}, view, opts, filteringForBucket, tf.forceFlatTasks)
// `view` is the foreign, attacker-controlled view — never validated against shareAuth.ProjectID
}
```
3. `getTaskOrTasksInBuckets` → `GetTasksInBucketsForView` (`pkg/models/kanban.go`) reads the buckets of that foreign view directly:
```go
err = s.Where("project_view_id = ?", view.ID).OrderBy("position").Find(&buckets)
...
users, err := getUsersOrLinkSharesFromIDs(s, userIDs) // resolves each bucket's created_by
```
Each returned `Bucket` serializes `CreatedBy *user.User` as `json:"created_by"` (`pkg/models/kanban.go`), exposing the creating user's username, name, and id.
The normal (non-share) path is protected because it runs `getRelevantProjectsFromCollection`, which calls `project.CanRead` on the URL project and returns 403 before any bucket data is produced. Only the `LinkSharing` branch skips that gate, which is why the bucket disclosure is link-share-specific. `ReadAllWeb` (`pkg/web/handler/read_all.go`) performs no permission check of its own, so all authorization for this endpoint lives inside `ReadAll`.
Scope of the leak (verified against fixtures): the *tasks* returned inside the buckets are still constrained to the share's own project (`getRawTasksForProjects` filters `tasks.project_id IN opts.projectIDs`, derived from the share's project). So victim task **contents do not leak**; what leaks is the foreign view's bucket structure and, critically, the `created_by` user identities of arbitrary buckets instance-wide.
Introduced in v0.24.0 by the per-view kanban feature (`feat(views)!: return tasks in buckets by view`), which added the `views/{view}/tasks` endpoint and the foreign-view bucket load. The link-share branch predates it, but the two only combine into this bug from v0.24.0 onward.
## Impact
A holder of a link-share token to any single project (link shares are designed to be handed out, often semi-publicly) can:
- **Enumerate all project and view IDs instance-wide.** IDs are sequential auto-increment integers; an invalid `(project, view)` pair returns 404 while a valid one returns 200, giving a reliable existence oracle across all tenants.
- **Disclose the `created_by` user of any kanban view's buckets** — username, display name, and user id. Because Vikunja auto-creates a default project with a Kanban view for every user, iterating view IDs enumerates usernames and user IDs for essentially every account on the instance.
- **Disclose bucket titles** of every kanban view in the instance.
This is a cross-tenant broken-object-level-authorization bypass. It does not disclose victim task contents through this path, but instance-wide username/user-id enumeration plus kanban structure disclosure is a meaningful PII and reconnaissance exposure that defeats the tenant isolation the permission model is meant to enforce. The ID-enumeration oracle additionally works for any ordinary authenticated user (invalid combo → 404 vs. inaccessible combo → 403), because the view is loaded before the `CanRead` check.
## Proof of Concept
Prerequisites: link sharing enabled (default), a share link to any project (call it project A), and any second project B (e.g. another tenant's) with a kanban view V_B.
1. Authenticate the share link to obtain a share JWT:
`POST /api/v1/shares/{shareHash}/auth`
2. Using that token, request a foreign kanban view's tasks:
`GET /api/v1/projects/{B}/views/{V_B}/tasks`
3. The 200 response is a list of `Bucket` objects belonging to view `V_B` — each with `title` and a populated `created_by` (username, name, id) — even though the token has no relationship to project B.
4. Iterate `{B}`/`{V_B}` over sequential integers: valid pairs return bucket lists (harvest usernames/ids), invalid pairs return 404.
Verified locally against the model fixtures: a `LinkSharing{ProjectID: 1}` (project 1 owned by user 1) calling `TaskCollection{ProjectID: 2, ProjectViewID: 8}.ReadAll` (project 2 owned by user 3) returns project 2's buckets (`"testbucket4 - other project"`, `"testbucket40"`) with their `created_by` user objects populated, and no error — while a nonexistent view id returns `ErrProjectViewDoesNotExist`.
## Recommended Fix
In `TaskCollection.ReadAll`, before th…
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
- github.com/go-vikunja/vikunja/security/advisories/GHSA-rj9j-8772-4h6c
- nvd.nist.gov/vuln/detail/CVE-2026-68582
- github.com/go-vikunja/vikunja/releases/tag/v2.4.0
- www.vulncheck.com/advisories/vikunja-broken-object-level-authorization-via-link-share-token
- github.com/advisories/GHSA-rj9j-8772-4h6c
Merged from GitHub Advisory Database, OSV. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON