# 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

Read-only Vikunja project members could read a link share's secret hash via the single-share endpoint, exchange it for a share JWT, and gain write/admin access.

- Payout: Research
- Severity: high
- Program: Vikunja
- Found by: ybsun0215, JellowBeanz26, 0xcelien
- Disclosed: 2026-10-09
- CVE: CVE-2026-91985
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-qfwc-vx6f-3g6g
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-qfwc-vx6f-3g6g
- Tags: area:web domain:api domain:web impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:go technique:incomplete-fix vuln:access-control vuln:info-disclosure

**Target:** REST API single link-share read endpoint (v1/v2)
**Root cause:** LinkSharing.CanRead delegates to project.CanRead, so any read-level member passes the gate, and the response serializes the share's bearer `hash` field (only `password` is cleared). The sibling ReadAll endpoint correctly requires project.IsAdmin.
**Steps:**
1. Owner creates project and read-write share: `PUT /api/v1/projects/123/shares` with `{"name":"poc","permission":1}` returns `{"id":7,"hash":"<SECRET_HASH>"}`
2. Owner adds attacker as read-only member: `PUT /api/v1/projects/123/users` with `{"username":"attacker","permission":0}`
3. Attacker reads single share: `GET /api/v1/projects/123/shares/7` with `$READER` token returns 200 with `hash` (v2 twin identical)
4. Attacker exchanges hash unauthenticated: `POST /api/v1/shares/<SECRET_HASH>/auth` with `{}` returns `{"token":"<LINK_JWT>"}`
5. Negative control: `PUT /api/v1/projects/123/tasks` with `$READER` returns 403
6. Escalation: same task create with `<LINK_JWT>` returns 201 Created
**How found:** Noticed the single-read endpoint's gate was never aligned with the list endpoint's fix (GHSA-8hp8 required project.IsAdmin for ReadAll); code reading showed CanRead delegates to project.CanRead and the response still serializes `hash` while clearing `password`.
**Exploitation:** A read-only project member enumerates small sequential share IDs, fetches the single-share record to obtain the hash, then POSTs the hash to the unauthenticated /shares/{hash}/auth endpoint to mint a link-share JWT at the share's permission level, using it to perform writes (or admin-level project settings/share management if an admin share exists). Password-protected shares still require the password.
**Escalation:** Chaining hash disclosure with the unauthenticated auth exchange converts read-only membership into write or project-admin capability; admin-level shares additionally expose project settings and share management.
**Insight:** When a class of bug is fixed on a list endpoint, check the sibling single-object endpoint for the same model-level gate — here ReadAll was hardened to IsAdmin but CanRead still used project.CanRead, and the serializer cleared `password` but not the equally sensitive `hash`.

