GitHub Security Advisory: GHSA-g66v-54v9-52pr
Package: code.vikunja.io/api (GO)
Severity: MODERATE (CVSS 6.4)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 2.2.0
Patched in: 2.2.1

Summary: Vikunja has SSRF via Todoist/Trello Migration File Attachment URLs that Allows Reading Internal Network Resources

Details:
## Summary

The migration helper functions `DownloadFile` and `DownloadFileWithHeaders` in `pkg/modules/migration/helpers.go` make arbitrary HTTP GET requests without any SSRF protection. When a user triggers a Todoist or Trello migration, file attachment URLs from the third-party API response are passed directly to these functions, allowing an attacker to force the Vikunja server to fetch internal network resources and return the response as a downloadable task attachment.

## Details

The vulnerability exists because the migration HTTP client uses a plain `http.Client{}` with no URL validation, no private IP blocklist, no redirect restrictions, and no response size limit.

**Vulnerable code** in `pkg/modules/migration/helpers.go:38-59`:
```go
func DownloadFileWithHeaders(url string, headers http.Header) (buf *bytes.Buffer, err error) {
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	// ... headers added ...
	hc := http.Client{}
	resp, err := hc.Do(req)
	// ... no URL validation, no IP filtering ...
	buf = &bytes.Buffer{}
	_, err = buf.ReadFrom(resp.Body) // no size limit
	return
}
```

**Call site in Todoist migration** (`pkg/modules/migration/todoist/todoist.go:433-435`):
```go
if len(n.FileAttachment.FileURL) > 0 {
	buf, err := migration.DownloadFile(n.FileAttachment.FileURL)
```

The `FileURL` is deserialized directly from the Todoist Sync API response (`json:"file_url"` tag at line 125) with no validation.

**Call sites in Trello migration** (`pkg/modules/migration/trello/trello.go`):
- Line 263: `migration.DownloadFile(board.Prefs.BackgroundImage)` — board background
- Line 345: `migration.DownloadFileWithHeaders(attachment.URL, ...)` — card attachments
- Line 381: `migration.DownloadFile(cover.URL)` — card cover images

Notably, the webhooks module in the same codebase was recently patched (commit `8d9bc3e`) to add SSRF protection using the `daenney/ssrf` library, but this protection was **not applied** to the migration module — making this an incomplete fix.

**Attack flow:**
1. Attacker creates a Todoist account
2. Using the Todoist Sync API, attacker creates a note with `file_attachment.file_url` set to an internal URL (e.g., `http://169.254.169.254/latest/meta-data/iam/security-credentials/`)
3. Attacker authenticates to the target Vikunja instance and initiates a Todoist migration
4. Vikunja's server fetches the internal URL and stores the response body as a task attachment
5. Attacker downloads the attachment through the normal Vikunja API, reading the internal resource contents

## PoC

**Prerequisites:**
- Vikunja instance with Todoist migration enabled (admin has configured OAuth client ID/secret)
- Authenticated Vikunja user account
- Todoist account controlled by the attacker

**Step 1: Craft malicious Todoist data**

Using the Todoist Sync API, create a note with an internal URL as the file attachment:

```bash
curl -X POST "https://api.todoist.com/