Your Project Manager Is Leaking to the Next Project Over
TL;DR — Vikunja's ReadAll endpoint leaks link share hashes, and its attachment handler doesn't verify that an attachment belongs to the requested task. Chain them: authenticate with any public link share, discover admin-level share hashes, then enumerate sequential attachment IDs to download or delete every file on the instance. One public share link to full data breach. CVSS 9.1.
background
Vikunja is a self-hosted open-source project management tool — think Todoist or Trello but on your own server. It supports projects, tasks, file attachments, link sharing, webhooks, and data migration from Todoist and Trello. It's used by teams and individuals who want project management without sending their data to a SaaS provider.
The irony of what follows should not be lost. The entire value proposition is keeping your data under your control. These seven bugs mean that a single public share link — the kind you'd send to an external collaborator — can be leveraged to access every file on the instance.
the headline chain (CVSS 9.1)
Two authorization bugs combine into an instance-wide data breach. Each is independently exploitable, but together they're devastating.
vulnerability 1: link share hash disclosure
Vikunja's link sharing feature generates a random hash for each share. Possessing the hash grants the share's permission level — read-only, write, or admin. The hash is effectively a bearer token.
The LinkSharing.ReadAll() method returns all shares for a project to any user with read access. The response correctly clears the password field but leaves the hash field intact:
GET /api/v1/projects/1/shares
Authorization: Bearer <read-only-share-token>
[
{
"id": 1,
"hash": "abc123readonly",
"right": 0,
"password": ""
},
{
"id": 2,
"hash": "xyz789admin",
"right": 2,
"password": ""
}
]
A user who authenticated with a read-only share (right: 0) can now see the admin share's hash (xyz789admin, right: 2). They authenticate with the admin hash and gain full admin access to the project.
vulnerability 2: cross-project attachment IDOR
The GetTaskAttachment handler takes two parameters: a task ID and an attachment ID. The authorization logic checks whether the requesting user has access to the task. But when fetching the attachment, it queries by attachment ID alone — without verifying that the attachment belongs to that task:
GET /api/v1/tasks/1/attachments/500
Authorization: Bearer <token-with-access-to-task-1>
The handler verifies access to task 1 (which the user has), then fetches attachment 500 (which belongs to task 47 in a completely different project). The authorization check runs against the wrong object.
Attachment IDs are sequential integers. The attacker doesn't need to guess anything — they iterate from 1 to N:
for id in $(seq 1 1000); do
curl -s -o "attachment_${id}" \
-w "%{http_code} attachment_${id}\n" \
"https://vikunja.target/api/v1/tasks/1/attachments/${id}" \
-H "Authorization: Bearer ${TOKEN}"
done
Every 200 response is a file from someone else's project. The same endpoint supports DELETE, so the attacker can also destroy every attachment on the instance.
chaining them together
The full attack requires only one thing: a public link share URL. These are designed to be shared — that's their purpose.
- Obtain any public link share URL (shared on a wiki, in an email, posted in a chat)
- Authenticate using the share's hash to get an API token
- Call
/projects/{id}/sharesto enumerate all shares for the project - Find an admin-level share hash in the response
- Re-authenticate with the admin hash
- Enumerate attachment IDs sequentially (1, 2, 3, ..., N)
- Download or delete every file on the instance
The entire attack is automatable. From a single link share URL to full data exfiltration in seconds.
five more bugs
webhook credential exposure (CVE-2026-33677)
Webhook configurations include BasicAuth usernames and passwords. The API returns these credentials in plaintext to any project collaborator, including read-only users:
GET /api/v1/projects/1/webhooks
[{
"url": "https://internal.corp/api/deploy",
"auth": {
"username": "deploy-bot",
"password": "prod-deploy-secret-2026"
}
}]
If a team configured a webhook with credentials for their CI/CD pipeline, deployment infrastructure, or internal APIs, every read-only collaborator can see those credentials. The fix is straightforward — strip credentials from read-only API responses — but the exposure window covers every Vikunja instance that uses authenticated webhooks.
SSRF via migration URLs (CVE-2026-33675)
Vikunja supports importing data from Todoist and Trello. During migration, the server fetches file attachment URLs from the source platform. These URLs aren't validated against internal network ranges, so an attacker can supply internal addresses:
{
"attachments": [{
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}]
}
The server fetches the URL and returns the content. This is a standard SSRF via a migration/import feature — a pattern that appears across many applications that import data from external sources.
cross-project info disclosure via task relations (CVE-2026-33676)
Tasks can have relations: "blocks," "is blocked by," "relates to." These relations can reference tasks in other projects. When querying a task's relations, the API returns the related task's full details without checking whether the requesting user has access to the related task's project.
A user in Project A creates a relation to task #500 in Project B. The API returns task #500's title, description, due date, assignees, and labels — all from a project the user has no access to. The relation doesn't need to be legitimate; the attacker just needs to know (or guess) a task ID.
webhook SSRF via OpenID Connect (CVE-2026-33679)
During OpenID Connect authentication, Vikunja downloads the user's avatar from a URL provided by the OIDC provider. This download bypasses the application's SSRF protections that apply to other outbound requests. An attacker who controls an OIDC provider (or can manipulate the avatar URL) gets unrestricted SSRF from a different code path than the migration SSRF.
link share hash escalation (CVE-2026-33680)
This is vulnerability 1 from the chain above, reported independently. The hash disclosure in ReadAll enables escalation from any share permission level to the highest available share on the project. The fix is a single line: s.Hash = "" in the serialization loop.
the fix
Version 2.2.1 addresses all seven vulnerabilities:
- Share hashes are stripped from
ReadAllresponses before serialization - Attachment queries include
AND task_id = ?to scope fetches to the parent task - Webhook credentials are filtered from API responses for non-admin users
- Migration URL fetching validates against internal network ranges (RFC 1918, link-local, loopback)
- Task relation queries enforce authorization on both sides of the relation
- OIDC avatar downloads apply the same SSRF protections as other outbound requests
what this teaches
Two authorization patterns made this possible:
Returning secrets in API responses because they're "not the sensitive field." The password was cleared but the hash wasn't. Both are credentials — possessing either one grants access. If a value authorizes actions when presented to the server, it's a secret and must be treated as one.
Checking access on the wrong object. The attachment handler verified the user's access to the task, then served an attachment that belongs to a different task entirely. The task was the key, but the attachment was behind a different door. This is a variant of the confused deputy problem — the server acts on behalf of the user but confuses which resource the user actually has access to.
Sequential IDs amplify both patterns. When resource identifiers are predictable integers and authorization has any gap, the gap applies to every resource on the system. UUIDs wouldn't fix the authorization bugs, but they'd make exploitation require additional information disclosure.
CVE-2026-33675 through CVE-2026-33680, plus GHSA-2pv8-4c52-mf8j. Fixed in Vikunja 2.2.1.