CVE-2026-55067 Overview
Vikunja, an open-source self-hosted task management platform, contains a mass assignment vulnerability in versions prior to 2.4.0. The endpoint POST /api/v1/projects/{project}/views/{view}/buckets/{bucket} accepts a project_view_id value in the request body that is mass-assigned by Bucket.Update in pkg/models/kanban.go. The permission check validates the bucket's current project and view from the URL path but fails to validate the destination view specified in the body. Any authenticated user can relocate an attacker-owned bucket into another tenant's Kanban view. This flaw is tracked as [CWE-639: Authorization Bypass Through User-Controlled Key].
Critical Impact
An authenticated attacker can inject an attacker-controlled bucket into another tenant's Kanban view, retaining ownership and content, enabling cross-tenant defacement.
Affected Products
- Vikunja versions prior to 2.4.0
- Self-hosted Vikunja task management deployments
- Multi-tenant Vikunja instances
Discovery Timeline
- 2026-08-28 - CVE-2026-55067 published to NVD
- 2026-08-28 - Last updated in NVD database
Technical Details for CVE-2026-55067
Vulnerability Analysis
The vulnerability resides in Vikunja's Kanban bucket update handler. When a client sends a POST request to /api/v1/projects/{project}/views/{view}/buckets/{bucket}, the server authorizes the request by verifying the caller's access to the project and view identified in the URL. It then hands the request body to Bucket.Update, which writes all supplied columns to the database, including project_view_id.
Because the destination project_view_id from the JSON payload is never checked against the authenticated user's permissions, an attacker who owns a bucket in their own project can transplant it into a victim tenant's view. The moved bucket keeps attacker-controlled title, content, and ownership. Victims see foreign content in their Kanban board and cannot easily distinguish it from legitimate data.
Root Cause
The root cause is over-broad mass assignment. The Update call in pkg/models/kanban.go included project_view_id in its list of writable columns, allowing clients to redirect the bucket to any view identifier. Authorization logic assumed the URL-scoped view was the only mutation target.
Attack Vector
Exploitation requires only a low-privilege authenticated account. The attacker creates a bucket inside a project they control, then issues an update request whose URL references their own project and view while the JSON body sets project_view_id to a target tenant's view ID. No user interaction on the victim side is required.
// Patch in pkg/models/kanban.go removes project_view_id from writable columns
"title",
"limit",
"position",
- "project_view_id",
).
Update(b)
return
Source: GitHub commit b31d606
// Migration importer keeps a scoped path to persist project_view_id safely
// project_view_id is intentionally not writable through Bucket.Update
// (blocks cross-tenant relocation, GHSA-569v). The importer legitimately
// remaps buckets onto same-project views, so persist that column directly.
persistBucketView := func(b *models.Bucket) error {
_, err := s.Where("id = ?", b.ID).Cols("project_view_id").Update(b)
return err
}
Source: GitHub commit b31d606
Detection Methods for CVE-2026-55067
Indicators of Compromise
- Kanban buckets appearing in a project that were created by users with no membership in that project or its parent team.
- HTTP POST requests to /api/v1/projects/{project}/views/{view}/buckets/{bucket} where the JSON body's project_view_id differs from the {view} in the URL path.
- Database rows in the buckets table whose project_view_id maps to a view under a project the bucket's created_by_id cannot access.
- Sudden appearance of unexpected bucket titles or content on Kanban boards without corresponding audit entries from tenant users.
Detection Strategies
- Parse Vikunja access logs to flag bucket update requests whose body project_view_id does not equal the URL view identifier.
- Run periodic SQL consistency checks joining buckets, project_views, and projects to identify buckets whose owner lacks project membership.
- Alert on authenticated API sessions issuing bucket updates targeting multiple distinct project views within short time windows.
Monitoring Recommendations
- Forward Vikunja application and reverse-proxy logs to a centralized analytics platform and retain full request bodies for the bucket update endpoint.
- Monitor for anomalous authenticated user behavior, such as accounts touching buckets or views far outside their normal activity.
- Track version and patch levels of all Vikunja instances to confirm upgrades to 2.4.0 or later.
How to Mitigate CVE-2026-55067
Immediate Actions Required
- Upgrade all Vikunja instances to version 2.4.0 or later, which removes project_view_id from the mass-assignable column set.
- Audit the buckets table for rows whose created_by_id does not have access to the current project_view_id, and quarantine or remove suspicious entries.
- Rotate API tokens for any accounts that exhibited the request patterns described in the detection section.
Patch Information
The fix is delivered in Vikunja 2.4.0. The relevant change removes project_view_id from the Update call in pkg/models/kanban.go and introduces a scoped persistBucketView helper for the migration importer that legitimately needs to remap buckets within the same project. See the GitHub Security Advisory GHSA-569v-q83c-3j3g, Pull Request #3239, and the v2.4.0 release notes.
Workarounds
- Restrict access to the Vikunja API to trusted users only, using network controls or authenticated reverse-proxy allowlists until the upgrade can be deployed.
- Deploy a reverse-proxy or WAF rule that rejects POST requests to /api/v1/projects/{project}/views/{view}/buckets/{bucket} whose JSON body contains a project_view_id field.
- Temporarily disable self-registration to reduce the pool of authenticated attackers on public instances.
# Example NGINX rule to strip project_view_id from bucket update bodies
# (requires the ngx_http_lua_module)
location ~ ^/api/v1/projects/[0-9]+/views/[0-9]+/buckets/[0-9]+$ {
if ($request_method = POST) {
access_by_lua_block {
ngx.req.read_body()
local body = ngx.req.get_body_data() or ""
if body:find('"project_view_id"') then
ngx.exit(400)
end
}
}
proxy_pass http://vikunja_backend;
}
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

