Oct 10, 2026·5 min read·5 visits
When a collaborator is removed from a Vikunja project, active webhooks and link shares created by that user are not removed. The former collaborator continues receiving live task updates via webhook.
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 is an open-source, self-hosted task management platform written in Go (code.vikunja.io/api). The application exposes project collaboration features including team management, direct user access controls, project webhooks, and public/private link sharing. Authorization for project resources relies on granular permissions evaluated across user and team associations.
A authorization flaw exists in Vikunja versions v0.22.0 through v2.6.0 where revoking a user's access to a project fails to purge or invalidate associated persistent assets created by that user. Specifically, webhooks and link shares established by a collaborator remain active in the system database after access revocation occurs.
This flaw is classified under CWE-281 (Improper Authorization) and CWE-613 (Insufficient Session Expiration). Because outbound webhooks automatically trigger upon task creation or modification, a revoked user can exfiltrate sensitive project data without maintaining active API credentials or user session tokens.
The underlying defect stems from two structural gaps within the authorization model and revocation workflow logic inside pkg/models/. The primary intended cleanup routine is cleanupTaskMembersAfterTeamRemoval, located in pkg/models/teams.go. This routine evaluates whether a removed user retains project access via project.CanRead(s, &user.User{ID: memberID}). If CanRead evaluates to false, project-associated entities are purged.
The first gap lies in the incomplete cleanup set within pkg/models/teams.go. The deletion queries inside cleanupTaskMembersAfterTeamRemoval target only two table entities: task assignments (TaskAssginee) and notification subscriptions (Subscription). Webhooks (webhooks) and share links (link_shares) are omitted from the cleanup queries despite maintaining functional access to project resources.
The second structural gap involves missing event dispatch triggers. cleanupTaskMembersAfterTeamRemoval is wired into only one of four possible revocation execution paths. While removing a user via team membership deletion (TeamMember.Delete) triggers the partial cleanup routine, direct removal of a user (ProjectUser.Delete), permission downgrades (ProjectUser.Update), and team unsharing (TeamProject.Delete) contain no invocation of the cleanup handler.
An examination of pkg/models/teams.go demonstrates the partial entity deletion logic during team access revocation. The execution logic isolates task assignees and subscriptions, leaving outbound integration mechanisms untouched.
// Partial cleanup logic in pkg/models/teams.go
_, err = s.In("task_id", taskIDs).And("user_id = ?", memberID).
Delete(&TaskAssginee{})
if err != nil {
return err
}
_, err = s.In("entity_id", taskIDs).
Where("entity_type = ? AND user_id = ?", SubscriptionEntityTask, memberID).
Delete(&Subscription{})
if err != nil {
return err
}
_, err = s.In("entity_id", projectsToCleanup).
Where("entity_type = ? AND user_id = ?", SubscriptionEntityProject, memberID).
Delete(&Subscription{})Because Webhooks and LinkShares are absent from this transaction, records containing created_by or shared_by matching memberID persist in the database. Furthermore, pkg/models/project_users.go handles direct user removal in ProjectUser.Delete without calling any derivative cleanup function.
When a project update event occurs, the event bus iterates through all registered webhooks for the project. Because the webhook entity remains registered, payload construction and HTTP dispatch proceed without checking whether created_by currently holds CanRead privileges.
Exploitation requires initial collaborator or write access to a target project. An attacker leverages valid credentials to register an HTTP webhook endpoint pointing to an external server under their control (POST /projects/{id}/webhooks). Additionally, the attacker can generate a project link share token (POST /projects/{id}/shares).
When administrators revoke the attacker's access—either by removing them directly from the project or removing them from an authorized team—the attacker's user session loses standard API authorization. Subsequent direct requests to /projects/{id} return HTTP 403 Forbidden.
Despite administrative revocation, the background system retains the webhook registration. Subsequent edits, comment additions, or task creations performed by remaining project members invoke the webhook worker. The system sends HTTP POST requests containing full task titles, descriptions, and metadata to the attacker's external endpoint.
This flaw results in persistent exfiltration of confidential project data over outbound network channels. CVSS v3.1 rates this issue at 6.5 (Medium) with vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N.
Confidentiality impact is high (C:H) because all subsequent project modifications generate real-time data payloads delivered directly to the revoked user. Integrity and availability are not directly affected by the webhook mechanism, though active link shares could allow unauthorized read access to project views post-removal.
Auditability remains possible via administrative API endpoints. Querying GET /projects/{id}/webhooks and GET /projects/{id}/shares exposes the created_by and shared_by user IDs, enabling administrators to identify lingering assets belonging to former team members.
Remediation requires structural updates to the cleanup framework and access checks across the application lifecycle. Developers must expand cleanupTaskMembersAfterTeamRemoval to include deletion of webhooks and link_shares where the creator ID matches the removed user ID.
Additionally, cleanup hooks must be bound to all four revocation execution paths (ProjectUser.Delete, ProjectUser.Update, TeamProject.Delete, and TeamMember.Delete). This guarantees cleanup execution regardless of how permission revocation is performed.
As a defense-in-depth measure, runtime validation should be implemented. Before dispatching a webhook payload or redeeming a link share, the system should evaluate project.CanRead for the user who created the asset. If the authorization check fails, delivery should be aborted and the stale asset purged.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Vikunja API Vikunja | >= v0.22.0, <= v2.6.0 | Post-v2.6.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-281 (Improper Authorization) |
| Secondary CWE | CWE-613 (Insufficient Session Expiration) |
| CVSS v3.1 Score | 6.5 (Medium) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Attack Vector | Network |
| Privileges Required | Low (Prior collaborator access) |
| Exploit Status | Proof of Concept / Technical Analysis Available |
| CISA KEV Status | Not Listed |
The software does not properly perform authorization checks or fails to revoke access permissions when user relationships change.
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.
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.
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.