GHSA-8cmm-j6c4-rr8v CVE-2026-33676 GO-2026-4847
Vikunja has Cross-Project Information Disclosure via Task Relations — Missing Authorization Check on Related Task Read
- Severity
- 6.5 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.36%
- Chance of exploitation in the next 30 days. Higher than 30.07% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-863 Incorrect Authorization
- Fix
2.2.1- Upgrade code.vikunja.io/api to this version or later
Description
Summary
When the Vikunja API returns tasks, it populates the related_tasks field with full task objects for all related tasks without checking whether the requesting user has read permission on those tasks' projects. An authenticated user who can read a task that has cross-project relations will receive full details (title, description, due dates, priority, percent completion, project ID, etc.) of tasks in projects they have no access to.
Details
The vulnerability is in addRelatedTasksToTasks() at pkg/models/tasks.go:496-548. This function is called by addMoreInfoToTasks() (line 773) during every task read operation — both project task listings (GET /api/v1/projects/{id}/views/{id}/tasks) and single task reads (GET /api/v1/tasks/{id}).
The function fetches all related tasks directly from the database without any permission filtering:
// pkg/models/tasks.go:496-548
func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task, a web.Auth) (err error) {
relatedTasks := []*TaskRelation{}
err = s.In("task_id", taskIDs).Find(&relatedTasks)
// ...
fullRelatedTasks := make(map[int64]*Task)
err = s.In("id", relatedTaskIDs).Find(&fullRelatedTasks) // Line 514: NO permission check
// ...
for _, rt := range relatedTasks {
// Directly adds to response without checking if user can read the related task
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], otherTask)
}
}
The a web.Auth parameter is received but only used for determining favorites (line 519), never for access control on the related tasks themselves.
In contrast, addBucketsToTasks() (line 550+) in the same file correctly filters enrichment data by calling getAllRawProjects(s, a, ...) to scope results to projects the requesting user can access.
While task relation creation properly enforces authorization (task_relation_permissions.go:32-52 checks write access on the base task and read access on the other task), the relation display path does not re-check permissions for the current reader. This means a privileged user can create a relation that then leaks data to all other users who can read the base task.
PoC
Setup: Two users (User A, User B), two projects (Project-Shared, Project-Private).
- User A has access to both projects.
- User B has access only to Project-Shared.
- Task 1 exists in Project-Shared, Task 2 exists in Project-Private.
Step 1: User A creates a relation between the two tasks
# As User A (who has access to both projects)
curl -X PUT "http://localhost:3456/api/v1/tasks/TASK1_ID/relations" \
-H "Authorization: Bearer USER_A_TOKEN" \
-H "Content-Type: application/json" \
-d '{"other_task_id": TASK2_ID, "relation_kind": "related"}'
Expected: 201 Created (User A has write on Task 1, read on Task 2).
Step 2: User B reads tasks from the shared project
# As User B (who has NO access to Project-Private)
curl "http://localhost:3456/api/v1/projects/PROJECT_SHARED_ID/views/VIEW_ID/tasks" \
-H "Authorization: Bearer USER_B_TOKEN"
Expected: Task 1 should be returned, but related_tasks should NOT include Task 2.
Actual result: The response includes Task 1 with the related_tasks field containing the full Task 2 object, including its title, description, due_date, priority, percent_done, project_id, and other metadata — despite User B having no access to Project-Private.
Impact
- Information disclosure: Any authenticated user can read the full metadata of tasks in projects they do not have access to, as long as a relation exists from a task they can read.
- Leaked fields include: title, description, due dates, start dates, priority, percent completion, project ID, hex color, task index, done status, repeat configuration, cover image attachment ID, and creation/update timestamps.
- Project structure disclosure: The
project_idfield reveals the existence and IDs of private projects. - No user interaction required: Once a privileged user creates a cross-project relation (which is intentionally allowed), the data leak is automatic for all readers of the base task.
- Blast radius: Affects all Vikunja instances with cross-project task relations. In multi-tenant or team environments where projects have different access scopes, this undermines project-level access control.
Recommended Fix
Filter related tasks by the requesting user's read permissions before adding them to the response. In addRelatedTasksToTasks(), after fetching full task objects, check that the user can read each related task's project:
func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task, a web.Auth) (err error) {
relatedTasks := []*TaskRelation{}
err = s.In("task_id", taskIDs).Find(&relatedTasks)
if err != nil {
return
}
var relatedTaskIDs []int64
for _, rt := range relatedTasks {
relatedTaskIDs = append(relatedTaskIDs, rt.OtherTaskID)
}
if len(relatedTaskIDs) == 0 {
return
}
fullRelatedTasks := make(map[int64]*Task)
err = s.In("id", relatedTaskIDs).Find(&fullRelatedTasks)
if err != nil {
return
}
// Filter related tasks by user's read permission
allowedProjectIDs := make(map[int64]bool)
checkedProjectIDs := make(map[int64]bool)
for _, t := range fullRelatedTasks {
if checkedProjectIDs[t.ProjectID] {
continue
}
checkedProjectIDs[t.ProjectID] = true
p := &Project{ID: t.ProjectID}
canRead, _, err := p.CanRead(s, a)
if err != nil {
log.Errorf("Could not check project read permission: %v", err)
continue
}
if canRead {
allowedProjectIDs[t.ProjectID] = true
}
}
taskFavorites, err := getFavorites(s, relatedTaskIDs, a, FavoriteKindTask)
if err != nil {
return err
}
for _, rt := range relatedTasks {
task, has := fullRelatedTasks[rt.OtherTaskID]
if !has {
continue
}
// Skip related tasks the user cannot access
if !allowedProjectIDs[task.ProjectID] {
continue
}
fullRelatedTasks[rt.OtherTaskID].IsFavorite = taskFavorites[rt.OtherTaskID]
otherTask := &Task{}
err = copier.Copy(otherTask, fullRelatedTasks[rt.OtherTaskID])
if err != nil {
log.Errorf("Could not duplicate task object: %v", err)
continue
}
otherTask.RelatedTasks = nil
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], otherTask)
}
return
}
This checks project-level read permission once per unique project ID (cached in allowedProjectIDs) and skips related tasks from projects the user cannot access.
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| code.vikunja.io/api | < 2.2.1 |
2.2.1 |
| code.vikunja.io/api | all versions |
No fix yet |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
code.vikunja.io/api Go → 2.2.1
- go
go get code.vikunja.io/api@v2.2.1 && go mod tidy
Severity breakdown
CVSS 3.1 6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Weakness
CWE-863: Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.
How to prevent it
- Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
- Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
- Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
- System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-m687-p538-r5hp Vikunja: Permissive Cross-domain Security Policy trusts every localhost origin which should not be trusted | Medium | Also affects code.vikunja.io/api |
| GHSA-fmmf-xq98-g327 Vikunja: Write-level project members can delete admin-tier link shares through an unloaded permission check | Medium | Also affects code.vikunja.io/api |
| GHSA-pjr3-86v4-5p7w Vikunja: Explicit lower-permission share on a sub-project is silently overridden by an inherited parent permission (broken access control / privilege-management regression in v2.6.0) | Medium | Also affects code.vikunja.io/api |
| GHSA-hjx8-qv73-f7cm Vikunja: Webhooks and link shares survive every revocation path, so a removed collaborator keeps a live feed | High | Also affects code.vikunja.io/api |
| GHSA-fprf-r6rv-xg99 Vikunja: Saved filter creation with an empty filter string recalculates task positions across all tenants | Medium | Also affects code.vikunja.io/api |
| GHSA-4hv6-xc92-j86g Vikunja: WebSocket authentication ignores server-side session state, so revoked sessions keep receiving live pushes | High | Also affects code.vikunja.io/api |
| GHSA-w2ch-4xgr-22ww Vikunja: Task relation deletion does not check read access to the other task, allowing cross-project relation removal | Low | Also affects code.vikunja.io/api |
| GHSA-8wvg-r2j4-3737 Vikunja: Assignee email addresses disclosed to read-only project members via the task assignees endpoint | Medium | Also affects code.vikunja.io/api |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor