Oct 10, 2026·6 min read·5 visits
An SQL MAX aggregation flaw in Vikunja v2.6.0 project permission handling causes inherited parent privileges to override explicit child project restrictions, permitting read-only users to perform full administrative operations.
Vikunja v2.6.0 contains a permission inheritance regression in pkg/models/project_access.go where explicit down-restrictions on sub-projects are overridden by higher parent project permissions due to MAX aggregation across project tree nodes.
Vikunja is an open-source, self-hosted task management platform. In version v2.6.0, an access control regression was introduced into the project permission engine (code.vikunja.io/api). The component responsible for determining user access is implemented within pkg/models/project_access.go in the function getProjectAccessForUser.
The permission evaluation engine calculates a user's effective permission across nested project trees. Projects can form hierarchical parent-child relationships, where a parent project contains sub-projects. In standard access control models, explicit permission grants on a child node take precedence over inherited parent grants, allowing administrators to restrict access to specific sensitive sub-projects.
In Vikunja v2.6.0, the calculation engine computes the user's effective permission level by evaluating the mathematical maximum (MAX) across all accessible parent and child nodes in the project subtree. Consequently, if a user holds ADMIN permissions on a parent project and an explicit READ restriction on a sub-project, the access control engine resolves the permissions as MAX(ADMIN, READ), granting the user full administrative control over the restricted sub-project.
The root cause of GHSA-PJR3-86V4-5P7W is an architectural design regression in the SQL query structure within getProjectAccessForUser. In version v2.5.0, Vikunja evaluated tree permissions using window functions (ROW_NUMBER() OVER (...)) ordered by node proximity. This nearest-ancestor-wins approach ensured that direct explicit grants on a child project superseded inherited grants from parent nodes.
In version v2.6.0, the query was refactored to use a Common Table Expression (CTE) combining direct user grants, team grants, and recursive subtree propagation. The recursive CTE gathers all permission records associated with any ancestor or descendant project node in the hierarchy.
The outer query concludes with SELECT id, MAX(permission) AS permission FROM tree GROUP BY id. Because permissions are represented numerically (READ = 0, WRITE = 1, ADMIN = 2), applying MAX() aggregates all permission levels associated with the project identifier id across both direct grants and parent inheritance rows. Any higher inherited grant continuously overrides explicit lower direct grants, invalidating down-restrictions.
The permission resolution query executed in pkg/models/project_access.go uses recursive CTE syntax compatible with PostgreSQL, SQLite, and MySQL. The direct grants CTE aggregates project ownership, user-specific grants, and team-based grants.
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 idThe vulnerability exists in the final statement SELECT id, MAX(permission) AS permission FROM tree GROUP BY id. The recursive tree union matches parent projects to child projects via p.parent_project_id = t.id and assigns t.permission (the parent's permission) to the child project entry.
When tree is grouped by project id, the child project entry contains two rows: one row generated from the direct explicit grant (C, 0) and one row generated from parent propagation (C, 2). The MAX() operation selects value 2, causing complete failure of the down-restriction mechanism.
Exploitation requires low-privileged authenticated access to a Vikunja instance where project nesting is enabled. An attacker who has been assigned ADMIN permissions on a high-level project and subsequently placed in a restricted READ role on a sub-project can execute administrative requests against that sub-project.
In a standard scenario, an instance administrator creates a parent project (FinanceRoot, ID 18) and assigns user alice administrative access (permission: 2). The administrator then creates a sensitive child project (Q4-Payroll-CONFIDENTIAL, ID 19) under project 18 and explicitly adds alice with read-only access (permission: 0).
PUT /api/v1/projects/19/users HTTP/1.1
Host: target-instance.local
Authorization: Bearer <alice-token>
Content-Type: application/json
{"username":"bob","permission":2}Although alice is explicitly restricted to read-only access on sub-project 19, the server responds with HTTP/1.1 201 Created. The engine resolves alice's effective permission as 2 (ADMIN) due to MAX(0, 2) aggregation. Consequently, alice successfully grants administrative access on project 19 to arbitrary users, deletes sub-project resources, or alters baseline project settings.
When alice attempts the exact same operation on an isolated standalone control project (ID 20) where she also holds explicit READ permission, the backend correctly returns HTTP/1.1 403 Forbidden. This differential response confirms that the privilege escalation occurs strictly due to subtree inheritance overriding.
The impact of GHSA-PJR3-86V4-5P7W affects confidentiality and integrity within multi-user task management environments. Project owners relying on sub-project permission down-restrictions to isolate sensitive information will have those restrictions bypassed.
An unauthorized user with inherited administrative rights can modify tasks, exfiltrate sensitive project attachments, delete project hierarchies, and grant access permissions to external accounts. Because the action executes under valid API endpoints with standard user tokens, standard log audit mechanisms will record the requests as legitimate user activity.
The vulnerability has a CVSS v3.1 base score of 5.4 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N). Authentication is required, but low-privileged credentials suffice to conduct exploitation if parent project access exists.
Remediation requires refactoring the permission resolution logic in pkg/models/project_access.go. The engine must prioritize explicit direct child grants over parent inheritance instead of evaluating the scalar maximum permission value across all tree paths.
Where database engines support window functions, the query should calculate path depth and assign priority based on proximity to the target node. Direct grants on the node itself must receive the highest precedence, followed by immediate parent nodes.
For instances running Vikunja v2.6.0 where source code modification is not immediately possible, administrators must implement operational mitigations. Avoid configuring lower permissions on sub-projects for users who possess higher permissions on ancestor projects. Sensitive sub-projects requiring restricted access must be detached from shared parent trees and placed in standalone project root hierarchies.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Vikunja API Vikunja | = 2.6.0 | None specified |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-269 / CWE-284 |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low (Authenticated User) |
| CVSS v3.1 Score | 5.4 (Medium) |
| Exploit Status | Proof of Concept Documented |
| CISA KEV Status | Not Listed |
The software does not properly manage privileges, allowing users to perform actions with higher permissions than intended.
Vikunja versions 2.3.0 through 2.6.0 contain an insufficient session expiration vulnerability (CWE-613) within the WebSocket authentication handler. Although Vikunja enforces server-side session tracking and revocation for REST API routes, the WebSocket handshake handler validates cryptographic JWT signatures without querying the database session state. Consequently, revoked JWT tokens can establish new real-time WebSocket connections, and existing connections persist after session revocation.
A cross-tenant boundary breach vulnerability in Vikunja allows an authenticated user to trigger global task position recalculations across all tenant instances by creating a saved filter with an empty filter string payload.
An access revocation flaw in Vikunja allows removed collaborators to retain outbound webhooks and link shares created prior to revocation, enabling persistent exfiltration of sensitive task data.
An authorization bypass vulnerability in Vikunja's link share deletion handlers allows project members with Write privileges to delete Admin-tier link shares. The handler passes an unpopulated struct to the authorization check, causing the permission evaluation to fall back to default Write permissions instead of requiring Admin privileges.
Vikunja versions 2.2.0 through 2.6.0 contain a Cross-Origin Resource Sharing (CORS) misconfiguration flaw in `code.vikunja.io/api`. Default configurations permit wildcard origins for localhost (`http://127.0.0.1:*` and `http://localhost:*`) with credentialed requests (`Access-Control-Allow-Credentials: true`). Because configuring a public service URL appends to this default list rather than overriding it, production environments inadvertently trust all local origins. A local page or application on a user's machine can execute a credentialed cross-origin request to the token refresh endpoint, extract the returned JWT access token, and achieve complete account takeover.
An insufficient session invalidation vulnerability exists in pyLoad (pyload-ng) versions 0.5.0b3.dev98 through 0.5.0b3.dev101. When administrative actions like privilege revocation or password changes are executed via the public REST API, active user sessions on disk are not updated or invalidated. Consequently, affected sessions remain fully authenticated with stale permissions for up to 31 days.