GHSA-96q5-xm3p-7m84

Suggest an improvement
Source
https://github.com/advisories/GHSA-96q5-xm3p-7m84
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-96q5-xm3p-7m84/GHSA-96q5-xm3p-7m84.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-96q5-xm3p-7m84
Aliases
Published
2026-04-10T15:31:11Z
Modified
2026-07-21T13:55:25Z
Severity
  • 6.5 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N CVSS Calculator
Summary
Vikunja: Link Share JWT tokens remain valid for 72 hours after share deletion or permission downgrade
Details

Title

Link Share JWT tokens remain valid for 72 hours after share deletion or permission downgrade

Description

Vikunja's link share authentication constructs authorization objects entirely from JWT claims without any server-side database validation. When a project owner deletes a link share or downgrades its permissions, all previously issued JWTs continue to grant the original permission level for up to 72 hours (the default service.jwtttl).

GetLinkShareFromClaims at pkg/models/link_sharing.go lines 88-119 performs zero database queries — it builds the LinkSharing struct purely from JWT claim values (id, hash, project_id, permission, sharedByID). This struct is passed directly to permission checks:

Function File Lines DB queries
GetLinkShareFromClaims link_sharing.go 88-119 0
Project.CanRead (link share) project_permissions.go 105-108 0
Project.CanWrite (link share) project_permissions.go 50-53 0
Project.IsAdmin (link share) project_permissions.go 192-194 0

Contrast with user tokens: User JWTs use a 10-minute TTL (ServiceJWTTTLShort) with sid claim and server-side sessions enabling revocation. Link share JWTs use a 72-hour TTL (ServiceJWTTTL) with no sid, no server-side session, and no refresh mechanism.

Permalink:

  • GetLinkShareFromClaims: pkg/models/link_sharing.go:88-119
  • NewLinkShareJWTAuthtoken: pkg/modules/auth/auth.go:141-160
  • Permission checks: pkg/models/project_permissions.go:50-53, 105-108, 192-194
  • TTL defaults: pkg/config/config.go:337-339

PoC

# 1. Create an Admin-level link share on project 42
curl -X PUT "https://vikunja.example.com/api/v1/projects/42/shares" \
  -H "Authorization: Bearer <owner-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"permission": 2}'
# Response: {"id": 5, "hash": "abc123", ...}

# 2. Obtain link share JWT (72h TTL, no sid claim)
curl -X POST "https://vikunja.example.com/api/v1/shares/abc123/auth"
# Response: {"token": "<link-share-jwt>"}

# 3. Delete the link share
curl -X DELETE "https://vikunja.example.com/api/v1/projects/42/shares/5" \
  -H "Authorization: Bearer <owner-jwt>"
# 200 OK — share row removed from database

# 4. Use the deleted share's JWT — STILL WORKS for up to 72 hours
curl -X GET "https://vikunja.example.com/api/v1/projects/42/tasks" \
  -H "Authorization: Bearer <link-share-jwt>"
# 200 OK — full task list returned with Admin permissions

# 5. Permission downgrade variant:
# Delete Admin share → create Read-only share → old JWT still has Admin access

Impact

  • Revoked link shares remain functional for up to 72 hours (default TTL)
  • Project owners cannot respond to security events (leaked URLs, access revocation) in real time
  • Permission downgrades have no effect on outstanding tokens
  • Scope: single project per token, severity scales with permission level (Admin > Write > Read)

Fix

Add database validation in GetLinkShareFromClaims:

func GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error) {
    id, is := claims["id"].(float64)
    if !is {
        return nil, &ErrLinkShareTokenInvalid{}
    }
    // Validate against database
    s := db.NewSession()
    defer s.Close()
    share, err = GetLinkShareByID(s, int64(id))
    if err != nil {
        return nil, err  // Share was deleted
    }
    // Verify permission not downgraded
    claimedPermission := Permission(claims["permission"].(float64))
    if share.Permission < claimedPermission {
        return nil, &ErrLinkShareTokenInvalid{}
    }
    return share, nil
}

Alternatives: shorter TTL with refresh mechanism, token blocklist, or session tracking matching user token pattern.

Database specific
{
    "cwe_ids":  [
        "CWE-613"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-04-10T15:31:11Z",
    "nvd_published_at":  "2026-04-10T16:16:32Z",
    "severity":  "MODERATE"
}
References

Affected packages

Go / code.vikunja.io/api

Package

Name
code.vikunja.io/api
View open source insights on deps.dev
Purl
pkg:golang/code.vikunja.io/api

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
2.3.0

Database specific

last_known_affected_version_range
"<= 2.2.2"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-96q5-xm3p-7m84/GHSA-96q5-xm3p-7m84.json"