GitHub Security Advisory: GHSA-2pv8-4c52-mf8j
Package: code.vikunja.io/api (GO)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-639: Authorization Bypass Through User-Controlled Key
Vulnerable versions: <= 2.2.0
Patched in: 2.2.1

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

Details:
## Summary

Two independently-exploitable authorization flaws in Vikunja can be chained to allow an unauthenticated attacker to download and delete every file attachment across all projects in a Vikunja instance. The `ReadAll` endpoint for link shares exposes share hashes (including admin-level shares) to any user with read access, enabling permission escalation. The task attachment `ReadOne`/`GetTaskAttachment` endpoint performs permission checks against a user-supplied task ID but fetches the attachment by its own sequential ID without verifying the attachment belongs to that task, enabling cross-project file access.

## Details

### Vulnerability 1: Link Share Hash Disclosure (Permission Escalation Entry Point)

Tracked in https://github.com/go-vikunja/vikunja/security/advisories/GHSA-8hp8-9fhr-pfm9

The `LinkSharing.ReadAll()` method in `pkg/models/link_sharing.go:228-287` returns all link shares for a project, including the `Hash` field:

```go
// pkg/models/link_sharing.go:46-50
type LinkSharing struct {
    ID   int64  `xorm:"bigint autoincr not null unique pk" json:"id" param:"share"`
    Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"`  // ← exposed in JSON
    // ...
}
```

The ReadAll clears passwords but not hashes:
```go
// pkg/models/link_sharing.go:272-277
for _, s := range shares {
    if sharedBy, has := users[s.SharedByID]; has {
        s.SharedBy = sharedBy
    }
    s.Password = ""  // ← password cleared, but hash remains
}
```

A link share user with read-only access can call `GET /api/v1/projects/:project/shares` (routed at `pkg/routes/routes.go:483`) to discover all shares, then authenticate with an admin-level share hash.

### Vulnerability 2: Cross-Project Attachment IDOR (Data Exfiltration)

Tracked in https://github.com/go-vikunja/vikunja/security/advisories/GHSA-jfmm-mjcp-8wq2

The `GetTaskAttachment` handler in `pkg/routes/api/v1/task_attachment.go:156-186` performs the permission check against the task ID supplied in the URL:

```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 from URL parameter
    return t.CanRead(s, a)     // ← checks if user can read THIS task
}
```

But `ReadOne` fetches the attachment by its own ID, ignoring the task:

```go
// pkg/models/task_attachment.go:110-111
func (ta *TaskAttachment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
    exists, err := s.Where("id = ?", ta.ID).Get(ta)  // ← fetches by attachment ID only
    // ta.TaskID is now overwritten with the ACTUAL task ID from the database
    // But the permission check already passed using the attacker-controlled task ID
```

This means: specify a task you CAN access, but an attachment ID from a different project → permission check passes, wrong attachment is returned.

### The Chain

```
Link share URL (public)
    → POST /shares/{hash}/auth (get JWT)
    → GET /projec