GitHub Security Advisory: GHSA-jfmm-mjcp-8wq2
Package: code.vikunja.io/api (GO)
Severity: HIGH (CVSS 8.1)
CWE: CWE-639: Authorization Bypass Through User-Controlled Key
Vulnerable versions: <= 2.2.0
Patched in: 2.2.1

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

Details:
## Summary

`TaskAttachment.ReadOne()` queries attachments by ID only (`WHERE id = ?`), ignoring the task ID from the URL path. The permission check in `CanRead()` validates access to the task specified in the URL, but `ReadOne()` loads a different attachment that may belong to a task in another project. This allows any authenticated user to download or delete any attachment in the system by providing their own accessible task ID with a target attachment ID. Attachment IDs are sequential integers, making enumeration trivial.

## Details

The vulnerability is in `pkg/models/task_attachment.go` in the `ReadOne` method:

```go
// pkg/models/task_attachment.go:110-120
func (ta *TaskAttachment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
	exists, err := s.Where("id = ?", ta.ID).Get(ta)  // Only checks attachment ID, ignores TaskID
	if err != nil {
		return
	}
	if !exists {
		return ErrTaskAttachmentDoesNotExist{
			TaskID:       ta.TaskID,
			AttachmentID: ta.ID,
		}
	}
	// ...
}
```

The permission check in `pkg/models/task_attachment_permissions.go` validates access to the URL task, not the attachment's actual task:

```go
// pkg/models/task_attachment_permissions.go:25-28
func (ta *TaskAttachment) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
	t := &Task{ID: ta.TaskID}  // ta.TaskID is from URL param :task
	return t.CanRead(s, a)
}
```

The `TaskAttachment` struct binds URL parameters via struct tags (`param:"task"` and `param:"attachment"`):
```go
// pkg/models/task_attachment.go:41-42
ID     int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"attachment"`
TaskID int64 `xorm:"bigint not null" json:"task_id" param:"task"`
```

**Attack flow for read (GET):**
The custom handler at `pkg/routes/api/v1/task_attachment.go:156` calls `CanRead` (checks URL task) then `ReadOne` (loads attachment by ID only).

**Attack flow for delete (DELETE):**
The generic CRUD handler calls `CanDelete` (checks write on URL task) then `Delete` which calls `ReadOne` (loads any attachment by ID), then deletes it.

This is the same vulnerability pattern that was already fixed for task comments, where `getTaskCommentSimple` was patched to add `AND task_id = ?` validation:

```go
// pkg/models/task_comments.go:196-205 (the fix)
func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
	query := s.Where("id = ?", tc.ID).NoAutoCondition()
	if tc.TaskID != 0 {
		query = query.And("task_id = ?", tc.TaskID)
	}
	// ...
}
```

## PoC

**Prerequisites:** Two users (attacker and victim). Victim has a project with a task that has a file attachment. Attacker has read access to any task (e.g., their own project).

**Step 1:** Attacker creates their own project and task.

```bash
# Attacker creates a project
curl -s -X PUT 'http://localhost:3456/api/v1/projects' \
  -H 'Authorization: Bearer <attacker_token>' \
  -H 'Content-Type: application/json' \
  -d '{"title":"attacker project"}' | jq '.id'
# Returns: 10

# Attacker creates a task in their p