Oct 10, 2026·5 min read·3 visits
Vikunja's WebSocket authentication pipeline skips database session state validation, allowing revoked JWT tokens to establish new real-time event streams and permitting existing connections to leak data indefinitely post-revocation.
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.
Vikunja (code.vikunja.io/api) is an open-source project and task management platform. To provide real-time updates across multi-user workspaces, Vikunja exposes a WebSocket endpoint at GET /api/v2/ws. This endpoint streams live events including task modifications, comment notifications, and project metadata updates to connected clients.
Beginning in version 2.3.0 and refined through version 2.6.0, Vikunja implemented server-side session tracking and revocation capabilities. Users can view and delete active sessions via DELETE /api/v2/user/sessions/{id}. Furthermore, security actions such as enabling Two-Factor Authentication (TOTP) execute DeleteAllUserSessions (pkg/routes/api/v2/user_totp.go:208) to invalidate all active user sessions across devices.
However, a design mismatch exists between the HTTP REST middleware and the WebSocket authentication pipeline. While HTTP REST routes check the session database table before fulfilling requests, the WebSocket endpoint verifies only the cryptographic signature and claim types of the user JWT token. As a result, the WebSocket authentication pipeline fails to enforce session revocation, violating the expected security boundaries of CWE-613.
The root cause resides in the decoupled token validation mechanism used during the WebSocket authentication handshake in pkg/websocket/connection.go. When a client initiates a connection to /api/v2/ws, the server invokes Connection.handleAuth to process authentication claims.
During handleAuth, token extraction and parsing delegate to auth.GetUserIDFromToken in pkg/modules/auth/auth.go. The function parses the JWT token using the configured service secret and checks internal claim fields. Specifically, it confirms that claims["type"] corresponds to AuthTypeUser and extracts the target user ID.
func GetUserIDFromToken(tokenString string) (int64, error) {
token, err := jwt.Parse(tokenString, func(_ *jwt.Token) (any, error) {
return []byte(config.ServiceSecret.GetString()), nil
})
if err != nil {
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return 0, jwt.ErrTokenUnverifiable
}
typ, ok := claims["type"].(float64)
if !ok || int(typ) != AuthTypeUser {
return 0, jwt.ErrTokenInvalidClaims
}
// Session ID ("sid") claim present in claims["sid"] is ignored
return int64(claims["user_id"].(float64)), nil
}While NewUserJWTAuthtoken (pkg/modules/auth/auth.go:188) injects a session identifier claim (claims["sid"] = sessionID) into generated access tokens, GetUserIDFromToken does not evaluate the sid claim. No query is performed against the database session table to verify whether the session ID remains active.
The failure model involves both initial handshake authentication and post-connection session lifecycle management. The architectural divergence between REST endpoints and WebSocket channels creates an inconsistent security model across the application surface.
For standard REST HTTP endpoints, authentication middleware retrieves the sid claim from the JWT token and verifies its existence in the database sessions table. If an administrator or user revokes the session, subsequent REST requests fail with 401 Unauthorized regardless of the cryptographic validity of the JWT token.
In contrast, the WebSocket authentication pipeline relies exclusively on signature validity. Furthermore, when DeleteUserSession or DeleteAllUserSessions executes, no broadcast signal or cleanup event is dispatched to the WebSocket connection manager. Consequently, active sockets remain registered in the WebSocket hub, receiving real-time server events indefinitely.
Exploitation requires an attacker or unauthorized user to possess a signed, unexpired JWT token associated with a session that was deleted or revoked on the server. The workflow proceeds through four primary stages.
First, an authenticated user logs into Vikunja and obtains a valid JWT access token. The application embeds both the user ID (user_id) and session ID (sid) in the token payload.
Second, session revocation is triggered. This occurs when the user clicks 'Revoke Session' in the user settings UI, when an administrator revokes active sessions, or when TOTP two-factor authentication is configured, which executes DeleteAllUserSessions.
Third, the client attempts to use the revoked JWT token across both endpoints. An HTTP GET request to /api/v2/tasks is rejected with 401 Unauthorized because the session ID no longer exists in the database. However, a WebSocket connection request to /api/v2/ws using the same token succeeds, establishing a persistent channel.
Fourth, once connected to the WebSocket hub, the client automatically receives server-pushed notifications, including notification.created events. These payloads contain real-time updates on tasks, internal comments, and workspace project changes.
This vulnerability carries a CVSS v3.1 base score of 6.5 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N). The primary impact is confidentiality loss due to unauthorized access to real-time project updates and user notification feeds.
Because WebSocket connections can remain active for long durations without re-authentication, an attacker who obtains a short-lived access token can retain long-term monitoring access even after the legitimate user revokes all sessions or resets their credentials.
Remediation requires maintainers to modify pkg/websocket/connection.go. During Connection.handleAuth, the handler must extract the sid claim from the incoming JWT and perform a database lookup against the active sessions table prior to upgrading the HTTP connection to a WebSocket protocol.
Additionally, the connection manager must implement a session invalidation listener. When a session row is deleted from the database, the server must push an internal termination signal to the WebSocket hub to forcibly close all open socket instances tied to the revoked sid or user_id.
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 go-vikunja | >= 2.3.0, <= 2.6.0 | Not reported |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-613 |
| Attack Vector | Network (Authenticated WS Handshake with Revoked Token) |
| CVSS v3.1 Score | 6.5 (Medium) |
| Impact | Information Disclosure via Real-Time Server-Pushed Data Stream |
| Exploit Status | Proof of Concept Available |
| CISA KEV Status | Not Listed |
| Affected Component | pkg/websocket/connection.go & pkg/modules/auth/auth.go |
The software does not expire, or insufficiently expires, a session token, exposing an opportunity for attackers to reuse expired or revoked session tokens.
Nginx UI versions 2.5.0 through 2.5.10 contain an uncontrolled resource consumption vulnerability in the node authentication handler. Unauthenticated remote attackers can exhaust host disk storage and I/O resources by submitting large HTTP request bodies to node-signature endpoints prior to cryptographic signature validation.
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.
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.