GHSA-5pg6-m483-7vrg

ghsaUSosv

Vikunja has cross-tenant IDOR in kanban move-task endpoint via unauthorized body task_id

view advisory published 2026-08-28Licence: CC-BY-4.0

## Summary The kanban endpoint `POST /api/v1/projects/{project}/views/{view}/buckets/{bucket}/tasks` moves a task into a bucket. The task is identified by `task_id` in the **request body**. The endpoint's authorization check (`TaskBucket.CanUpdate`) only verifies that the caller may update the *project/view/bucket named in the URL* — it never checks any permission on `task_id`. Any authenticated user can therefore supply another user's task ID (task IDs are a global, sequential integer space) against a kanban bucket in their **own** project. The server loads that victim task with no authorization check, returns its full contents in the response, and — when the target bucket is a "done" bucket — writes to the victim task's row. This is the same vulnerability class Vikunja has already remediated for task relations (CVE-2026-33676), task attachments (CVE-2026-33678), task comments (CVE-2026-33313) and CalDAV task read (CVE-2026-35598). `TaskBucket` is the task-child operation that was missed. --- ## Root cause ### 1. `task_id` is body-controlled and never permission-checked `pkg/models/kanban_task_bucket.go:32`: type TaskBucket struct { BucketID int64 `... json:"bucket_id" param:"bucket"` TaskID int64 `... json:"task_id"` // body-bound only — no param tag ProjectViewID int64 `... json:"project_view_id" param:"view"` ProjectID int64 `xorm:"-" json:"-" param:"project"` ... } The web handler `UpdateWeb` (`pkg/web/handler/update.go`) populates the struct via `ctx.Bind`, which binds both URL path params (`param:` tags) **and** the JSON body. `BucketID`, `ProjectViewID`, `ProjectID` come from the trusted URL; `TaskID` comes entirely from the attacker-controlled body. ### 2. `CanUpdate` authorizes the URL, not the task `pkg/models/kanban_task_bucket.go:52`: func (b *TaskBucket) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { bucket := Bucket{ID: b.BucketID, ProjectID: b.ProjectID, ProjectViewID: b.ProjectViewID} return bucket.canDoBucket(s, a) } `canDoBucket` (`pkg/models/kanban_permissions.go:46`) resolves the bucket/view and ends in `Project{ID: pv.ProjectID}.CanUpdate(s, a)` — a permission check on the **project from the URL**. `b.TaskID` is never referenced. The attacker owns that project, so the check passes. ### 3. The task is loaded and mutated with no authorization `updateTaskBucket` (`pkg/models/kanban_task_bucket.go:119`): task := &Task{ID: b.TaskID} err = task.ReadOne(s, a) // loads ANY task by ID — no permission check `Task.ReadOne` (`pkg/models/tasks.go:1967`) calls `GetTaskByIDSimple` + `addMoreInfoToTasks`; it performs no authorization (authorization normally lives in the separate `Task.CanRead`, which this internal call path bypasses). - **Read:** the fully populated victim task is assigned to `b.Task` (line 227) and returned by the `Update` handler in the response `"task"` field. - **Write:** if the target bucket is the view's done bucket (`view.DoneBucketID == b.BucketID && !task.Done`, line 141), the handler sets `task.Done = true` and persists it to the victim's task row: _, err = s.Where("id = ?", task.ID). Cols("done", "due_date", "start_date", "end_date", "done_at"). Update(task) --- ## Proof of Concept The attacker is any normal authenticated user. They first create their own kanban project/view/bucket (free for every user), then: POST /api/v1/projects/{ATTACKER_PROJECT}/views/{ATTACKER_VIEW}/buckets/{ATTACKER_BUCKET}/tasks HTTP/1.1 Host: TARGET Authorization: Bearer {ATTACKER_JWT} Content-Type: application/json {"task_id": {VICTIM_TASK_ID}} The `200` response body contains the victim task in full under `"task"` — title, description, dates, assignees, labels, attachment list, reactions. Task IDs are a global sequential counter, so iterating `task_id` enumerates every task on the instance. If `ATTACKER_BUCKET` is the done bucket of `ATTACKER_VIEW`, the same request also flips the victim task to done (`done = true`, `done_at` set). --- ## Impact Any authenticated low-privilege user can: - **Read any task on the instance** by sequential ID, across every other user, project and organization — a full cross-tenant information disclosure of task titles, descriptions, assignees, labels and attachment metadata. - **Modify any task's done state**, marking arbitrary victims' tasks done (or clearing it) and altering `done_at`. Vikunja's permission model is built specifically to isolate projects between users; this endpoint defeats that isolation. It is the same impact and class that warranted CVEs for task relations, attachments and comments. --- ## Suggested fix In `TaskBucket.CanUpdate`, after the bucket/project check, also verify the caller's permission on the body-supplied task — mirroring the remediation already applied to task relations and attachments: task := &Task{ID: b.TaskID} canUpdateTask, err := task.CanUpdate(s, a) if err != nil || !canUpdateTask { return false, err } (Use `CanRead` if moving a readable-but-not-writable task into a bucket is intended; `CanUpdate` is the safer default since the operation can change the task's done state.) --- ## References - CWE-639 Authorization Bypass Through User-Controlled Key - CWE-284 Improper Access Control - OWASP A01:2021 Broken Access Control - CVE-2026-33676, CVE-2026-33678, CVE-2026-33313, CVE-2026-35598 — the same missing-authorization-on-task-child class, already remediated; this report is the un-remediated `TaskBucket` sibling. ## Additional notes - **The v2 API is affected too.** The same endpoint is exposed under `/api/v2/...`, and both versions route through the shared model `TaskBucket.CanUpdate` / `updateTaskBucket` in `pkg/models/kanban_task_bucket.go`. A model-level fix closes v1 and v2 simultaneously; the regression test should assert both. - **Two fix altitudes.** The minimal fix checks the body-supplied `task_id` in `TaskBucket.CanUpdate` (`task.CanUpdate`/`CanRead`). A broader fix makes `Task.ReadOne` itself permission-aware, which also hardens other internal call paths that rely on it — higher blast radius, weigh accordingly. - Side effects of the cross-tenant write confirmed across reports: flipping `done` rewrites `done_at`/`due_date`/`start_date`/`end_date`, inserts a `task_buckets` row, propagates done-state to other kanban views with a done bucket in the victim's project, and triggers `updateDone` rescheduling for repeating tasks.

01

describes (1)

Related CVEs.

IDseverity exploitable
CVE-2026-55066US——
Vikunja is an open-source self-hosted task management platform. Prior to 2.4.0, POST /api/v1/projects/{project}/views/{view}/buckets/{bucket}/tasks accepts a body supplied task_id…
02

references

External citations — patches, advisories, analysis.

scry

Advisories like this name the exposed products attackers go after on your perimeter. See what is exposed on your perimeter with a free passive scan.

Free scan