Oct 10, 2026·5 min read·8 visits
Write-level collaborators in Vikunja can delete admin-tier link shares due to an authorization check executing on an unpopulated model struct, causing stored admin permissions to evaluate as zero.
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 is an open-source, self-hosted task management platform written in Go (code.vikunja.io/api). The application provides link sharing functionality, enabling project owners and administrators to generate access tokens and URLs for external stakeholders with predefined permissions such as Read, Write, or Admin.
An authorization bypass bug exists in the HTTP handler responsible for deleting project link shares. Under intended access control logic, revoking or deleting an Admin-tier link share requires the requesting user to hold Admin privileges on the parent project. However, the deletion logic fails to retrieve the target link share's stored state from the database prior to evaluating access control.
Because authorization rules are evaluated against an uninitialized Go struct, the internal permission field defaults to zero (PermissionRead). Consequently, the access check grants deletion rights to any user with Write access to the project, violating expected authorization boundaries.
The root cause of GHSA-fmmf-xq98-g327 stems from incorrect object initialization prior to access control policy evaluation, classified under CWE-863 (Incorrect Authorization).
In pkg/routes/api/v2/link_sharing.go, the HTTP routing handler receives a request to delete a share given a project ID and share ID. Instead of querying the database for the existing LinkSharing record, the handler instantiates an incomplete struct populated only with the route parameter identifiers:
&models.LinkSharing{ID: in.ID, ProjectID: in.ProjectID}
This struct is passed to handler.DoDelete, which invokes LinkSharing.CanDelete. CanDelete delegates evaluation to canDoLinkShare in pkg/models/link_sharing_permissions.go.
Inside canDoLinkShare, the system evaluates whether share.Permission == PermissionAdmin. Because share is an unpopulated struct instance, share.Permission contains the Go zero-value for its integer type, which corresponds to PermissionRead (0) rather than PermissionAdmin (2). The system skips the required IsAdmin check and proceeds to CanWrite. Since the caller possesses Write rights on the project, the check succeeds.
The vulnerability is visible in the structural binding and decision logic across pkg/routes/api/v2/link_sharing.go and pkg/models/link_sharing_permissions.go.
// Vulnerable Flow in pkg/routes/api/v2/link_sharing.go
func (h *LinkSharingHandler) Delete(c labstack.Context) error {
var in struct {
ProjectID int64 `param:"project"`登
ID int64 `param:"share"`撘
}
// ... binding logic ...
// Unpopulated model created without database read
s := &models.LinkSharing{ID: in.ID, ProjectID: in.ProjectID}
return handler.DoDelete(c, s)
}During evaluation inside pkg/models/link_sharing_permissions.go:
func canDoLinkShare(s *m.Session, a m.Admin, share *LinkSharing) bool {
// share.Permission is 0 (zero-value) instead of 2 (PermissionAdmin)
if share.Permission == PermissionAdmin {
return l.IsAdmin(s, a)
}
// Unloaded check falls through to CanWrite
return l.CanWrite(s, a)
}Once CanDelete returns true, handler.DoDelete executes LinkSharing.Delete. The SQL query filters strictly by id and project_id, removing the database row without ever validating whether the actual record had PermissionAdmin assigned.
Exploitation requires network access to the Vikunja instance and an authenticated session for an account with Write collaborator privileges on a target project.
First, an attacker attempts to create an admin-tier link share to confirm authorization checks on creation endpoints are active:
curl -i -X POST "https://vikunja.local/api/v2/projects/10/shares" \
-H "Authorization: Bearer WRITER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"permission":2}'The server correctly returns HTTP 403 Forbidden. However, when targeting an existing admin-tier link share created by the project owner (e.g., share ID 42), the attacker issues a DELETE request:
curl -i -X DELETE "https://vikunja.local/api/v2/projects/10/shares/42" \
-H "Authorization: Bearer WRITER_TOKEN"Because share.Permission evaluates to 0 during authorization, the check succeeds, returning HTTP 204 No Content (or HTTP 200 OK on /api/v1/). The target admin-tier link share is permanently deleted from the database.
The impact of GHSA-fmmf-xq98-g327 is localized to project link share management and external session persistence.
An attacker holding Write access can destroy administrative share links set up by project owners. In configurations where link-share JWT tokens undergo database validation upon every request, deleting the link share immediately revokes external active sessions, resulting in HTTP 401 Unauthorized errors for external consumers.
The vulnerability does not grant vertical privilege escalation; the attacker cannot acquire administrative privileges within the project, read unauthorized project data, or create new administrative link shares.
To remediate this vulnerability, deletion handlers must pre-fetch the targeted LinkSharing record from the database using both share ID and project ID prior to executing authorization routines.
// Recommended Remediation Pattern
func (h *LinkSharingHandler) Delete(c labstack.Context) error {
// 1. Fetch existing record from DB
share, err := models.GetLinkShareByIDAndProject(in.ID, in.ProjectID)
if err != nil {
return err
}
// 2. Pass fully populated model to authorization and deletion handler
return handler.DoDelete(c, share)
}If immediate patching is not possible, instances should restrict Write collaborator access on critical projects or monitor access logs for DELETE operations targeting /api/v1/projects/*/shares/* and /api/v2/projects/*/shares/*.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Vikunja API Vikunja | >= 0.13.0, <= 2.6.0 | Unreleased (post-2.6.0 / main branch) |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-863 (Incorrect Authorization) |
| Attack Vector | Network |
| CVSS v4.0 Score | 5.3 (Medium) |
| EPSS Score | N/A (No CVE Assigned) |
| Impact | Unauthorized deletion of admin-tier project link shares |
| Exploit Status | Proof of Concept available |
| KEV Status | Not Listed |
The software performs an authorization check when an actor attempts to access a resource, but it does not correctly perform the check, allowing access without required privileges.
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.
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 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.