{"id":"CVE-2026-35594","aliases":["GHSA-96q5-xm3p-7m84","GO-2026-5276"],"title":"Vikunja: Link Share JWT tokens remain valid for 72 hours after share deletion or permission downgrade","summary":"Vikunja: Link Share JWT tokens remain valid for 72 hours after share deletion or permission downgrade","severity":"medium","cvss":6.5,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N","vendor":"api","product":"code.vikunja.io/api","ecosystem":"go","affected":["code.vikunja.io/api < 2.3.0"],"patched":["code.vikunja.io/api 2.3.0"],"published":"2026-04-10","updated":"2026-07-21","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-96q5-xm3p-7m84","references":[{"url":"https://github.com/go-vikunja/vikunja/security/advisories/GHSA-96q5-xm3p-7m84"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-35594"},{"url":"https://github.com/go-vikunja/vikunja/pull/2581"},{"url":"https://github.com/go-vikunja/vikunja/commit/379d8a5c19334ffe4846003f590e202c31a75479"},{"url":"https://github.com/go-vikunja/vikunja"},{"url":"https://github.com/go-vikunja/vikunja/releases/tag/v2.3.0"}],"tags":["osv","go"],"epss":0.00268,"epssPercentile":0.19175,"ingestedAt":"2026-07-21T19:04:58.597Z","slug":"CVE-2026-35594","body":"## Overview\n\n## Title\nLink Share JWT tokens remain valid for 72 hours after share deletion or permission downgrade\n\n## Description\n\nVikunja'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`).\n\n`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:\n\n| Function | File | Lines | DB queries |\n|----------|------|-------|------------|\n| `GetLinkShareFromClaims` | `link_sharing.go` | 88-119 | 0 |\n| `Project.CanRead` (link share) | `project_permissions.go` | 105-108 | 0 |\n| `Project.CanWrite` (link share) | `project_permissions.go` | 50-53 | 0 |\n| `Project.IsAdmin` (link share) | `project_permissions.go` | 192-194 | 0 |\n\n**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.\n\n**Permalink:**\n- `GetLinkShareFromClaims`: `pkg/models/link_sharing.go:88-119`\n- `NewLinkShareJWTAuthtoken`: `pkg/modules/auth/auth.go:141-160`\n- Permission checks: `pkg/models/project_permissions.go:50-53, 105-108, 192-194`\n- TTL defaults: `pkg/config/config.go:337-339`\n\n### PoC\n\n```bash\n# 1. Create an Admin-level link share on project 42\ncurl -X PUT \"https://vikunja.example.com/api/v1/projects/42/shares\" \\\n  -H \"Authorization: Bearer <owner-jwt>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"permission\": 2}'\n# Response: {\"id\": 5, \"hash\": \"abc123\", ...}\n\n# 2. Obtain link share JWT (72h TTL, no sid claim)\ncurl -X POST \"https://vikunja.example.com/api/v1/shares/abc123/auth\"\n# Response: {\"token\": \"<link-share-jwt>\"}\n\n# 3. Delete the link share\ncurl -X DELETE \"https://vikunja.example.com/api/v1/projects/42/shares/5\" \\\n  -H \"Authorization: Bearer <owner-jwt>\"\n# 200 OK — share row removed from database\n\n# 4. Use the deleted share's JWT — STILL WORKS for up to 72 hours\ncurl -X GET \"https://vikunja.example.com/api/v1/projects/42/tasks\" \\\n  -H \"Authorization: Bearer <link-share-jwt>\"\n# 200 OK — full task list returned with Admin permissions\n\n# 5. Permission downgrade variant:\n# Delete Admin share → create Read-only share → old JWT still has Admin access\n```\n\n### Impact\n\n- Revoked link shares remain functional for up to 72 hours (default TTL)\n- Project owners cannot respond to security events (leaked URLs, access revocation) in real time\n- Permission downgrades have no effect on outstanding tokens\n- Scope: single project per token, severity scales with permission level (Admin > Write > Read)\n\n### Fix\n\nAdd database validation in `GetLinkShareFromClaims`:\n\n```go\nfunc GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error) {\n    id, is := claims[\"id\"].(float64)\n    if !is {\n        return nil, &ErrLinkShareTokenInvalid{}\n    }\n    // Validate against database\n    s := db.NewSession()\n    defer s.Close()\n    share, err = GetLinkShareByID(s, int64(id))\n    if err != nil {\n        return nil, err  // Share was deleted\n    }\n    // Verify permission not downgraded\n    claimedPermission := Permission(claims[\"permission\"].(float64))\n    if share.Permission < claimedPermission {\n        return nil, &ErrLinkShareTokenInvalid{}\n    }\n    return share, nil\n}\n```\n\nAlternatives: shorter TTL with refresh mechanism, token blocklist, or session tracking matching user token pattern.\n\n## Affected packages\n\n- `code.vikunja.io/api < 2.3.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `code.vikunja.io/api 2.3.0`","depth":"sunlit","depthScore":36,"depthScoreParts":{"impact":35.8,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}