Skip to content

Commit a63cf33

Browse files
1 parent 412a0c8 commit a63cf33

5 files changed

Lines changed: 353 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-7c2g-p23p-4jg3",
4+
"modified": "2026-03-25T21:17:28Z",
5+
"published": "2026-03-25T21:17:28Z",
6+
"aliases": [
7+
"CVE-2026-33677"
8+
],
9+
"summary": "Vikjuna: Webhook BasicAuth Credentials Exposed to Read-Only Project Collaborators via API",
10+
"details": "## Summary\n\nThe `GET /api/v1/projects/:project/webhooks` endpoint returns webhook BasicAuth credentials (`basic_auth_user` and `basic_auth_password`) in plaintext to any user with read access to the project. While the existing code correctly masks the HMAC `secret` field, the BasicAuth fields added in a later migration were not given the same treatment. This allows read-only collaborators to steal credentials intended for authenticating against external webhook receivers.\n\n## Details\n\nWhen listing project webhooks, the `ReadAll` method in `pkg/models/webhooks.go` (line 203) only requires project read access:\n\n```go\n// pkg/models/webhooks.go:203-244\nfunc (w *Webhook) ReadAll(s *xorm.Session, a web.Auth, _ string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {\n\tp := &Project{ID: w.ProjectID}\n\tcan, _, err := p.CanRead(s, a) // Only requires read permission\n\tif err != nil {\n\t\treturn nil, 0, 0, err\n\t}\n\tif !can {\n\t\treturn nil, 0, 0, ErrGenericForbidden{}\n\t}\n\n\t// ... fetches webhooks from DB ...\n\n\tfor _, webhook := range ws {\n\t\twebhook.Secret = \"\" // HMAC secret is masked\n\t\t// BasicAuthUser and BasicAuthPassword are NOT masked\n\t\tif createdBy, has := users[webhook.CreatedByID]; has {\n\t\t\twebhook.CreatedBy = createdBy\n\t\t}\n\t}\n\n\treturn ws, len(ws), total, err\n}\n```\n\nThe `Webhook` struct defines both fields with JSON serialization tags, so they are included in API responses:\n\n```go\n// pkg/models/webhooks.go:63-64\nBasicAuthUser string `xorm:\"null\" json:\"basic_auth_user\"`\nBasicAuthPassword string `xorm:\"null\" json:\"basic_auth_password\"`\n```\n\nThe BasicAuth fields were added in migration `20260123000717` (\"Add basic auth to webhooks\"), but the credential masking logic at line 238 was not updated to include these new fields.\n\nThe same issue exists in the user webhook listing at `pkg/routes/api/v1/user_webhooks.go:65`, where `Secret` is masked but BasicAuth fields are not. This is lower impact since users only see their own webhooks.\n\n## PoC\n\n1. **As User A (project admin)**, create a project and a webhook with BasicAuth credentials:\n\n```bash\n# Create a webhook with BasicAuth on project 1\ncurl -X PUT \"http://localhost:3456/api/v1/projects/1/webhooks\" \\\n -H \"Authorization: Bearer $TOKEN_A\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"target_url\": \"https://external-service.example.com/hook\",\n \"events\": [\"task.created\"],\n \"secret\": \"my-hmac-secret\",\n \"basic_auth_user\": \"service-account\",\n \"basic_auth_password\": \"S3cretP@ssw0rd!\"\n }'\n```\n\n2. **As User B (read-only collaborator on the same project)**, list webhooks:\n\n```bash\ncurl -s \"http://localhost:3456/api/v1/projects/1/webhooks\" \\\n -H \"Authorization: Bearer $TOKEN_B\" | jq '.[0] | {secret, basic_auth_user, basic_auth_password}'\n```\n\n3. **Expected output** (secret is masked, but BasicAuth is leaked):\n\n```json\n{\n \"secret\": \"\",\n \"basic_auth_user\": \"service-account\",\n \"basic_auth_password\": \"S3cretP@ssw0rd!\"\n}\n```\n\n## Impact\n\n- **Credential theft**: Any user with read-only access to a project can steal BasicAuth credentials configured on that project's webhooks. These credentials may grant access to external services (CI/CD systems, notification endpoints, third-party APIs).\n- **Lateral movement**: Stolen credentials could be reused to authenticate against external systems that the webhook receiver protects.\n- **Broad exposure surface**: Credentials are exposed to all project readers, including users granted access through team shares and link shares (with read+ permission level).\n\n## Recommended Fix\n\nIn `pkg/models/webhooks.go`, add masking for BasicAuth fields alongside the existing `Secret` masking (around line 237):\n\n```go\nfor _, webhook := range ws {\n\twebhook.Secret = \"\"\n\twebhook.BasicAuthUser = \"\"\n\twebhook.BasicAuthPassword = \"\"\n\tif createdBy, has := users[webhook.CreatedByID]; has {\n\t\twebhook.CreatedBy = createdBy\n\t}\n}\n```\n\nApply the same fix in `pkg/routes/api/v1/user_webhooks.go` (around line 64):\n\n```go\nfor _, w := range ws {\n\tw.Secret = \"\"\n\tw.BasicAuthUser = \"\"\n\tw.BasicAuthPassword = \"\"\n\tif createdBy, has := users[w.CreatedByID]; has {\n\t\tw.CreatedBy = createdBy\n\t}\n}\n```",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "code.vikunja.io/api"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "2.2.1"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 2.2.0"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-7c2g-p23p-4jg3"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33677"
49+
},
50+
{
51+
"type": "PACKAGE",
52+
"url": "https://github.com/go-vikunja/vikunja"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://vikunja.io/changelog/vikunja-v2.2.2-was-released"
57+
}
58+
],
59+
"database_specific": {
60+
"cwe_ids": [
61+
"CWE-200"
62+
],
63+
"severity": "MODERATE",
64+
"github_reviewed": true,
65+
"github_reviewed_at": "2026-03-25T21:17:28Z",
66+
"nvd_published_at": "2026-03-24T16:16:35Z"
67+
}
68+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-8cmm-j6c4-rr8v",
4+
"modified": "2026-03-25T21:17:12Z",
5+
"published": "2026-03-25T21:17:12Z",
6+
"aliases": [
7+
"CVE-2026-33676"
8+
],
9+
"summary": "Vikunja has Cross-Project Information Disclosure via Task Relations — Missing Authorization Check on Related Task Read",
10+
"details": "## Summary\n\nWhen the Vikunja API returns tasks, it populates the `related_tasks` field with full task objects for all related tasks without checking whether the requesting user has read permission on those tasks' projects. An authenticated user who can read a task that has cross-project relations will receive full details (title, description, due dates, priority, percent completion, project ID, etc.) of tasks in projects they have no access to.\n\n## Details\n\nThe vulnerability is in `addRelatedTasksToTasks()` at `pkg/models/tasks.go:496-548`. This function is called by `addMoreInfoToTasks()` (line 773) during every task read operation — both project task listings (`GET /api/v1/projects/{id}/views/{id}/tasks`) and single task reads (`GET /api/v1/tasks/{id}`).\n\nThe function fetches all related tasks directly from the database without any permission filtering:\n\n```go\n// pkg/models/tasks.go:496-548\nfunc addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task, a web.Auth) (err error) {\n relatedTasks := []*TaskRelation{}\n err = s.In(\"task_id\", taskIDs).Find(&relatedTasks)\n // ...\n fullRelatedTasks := make(map[int64]*Task)\n err = s.In(\"id\", relatedTaskIDs).Find(&fullRelatedTasks) // Line 514: NO permission check\n // ...\n for _, rt := range relatedTasks {\n // Directly adds to response without checking if user can read the related task\n taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(\n taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], otherTask)\n }\n}\n```\n\nThe `a web.Auth` parameter is received but only used for determining favorites (line 519), never for access control on the related tasks themselves.\n\nIn contrast, `addBucketsToTasks()` (line 550+) in the same file correctly filters enrichment data by calling `getAllRawProjects(s, a, ...)` to scope results to projects the requesting user can access.\n\nWhile task relation **creation** properly enforces authorization (`task_relation_permissions.go:32-52` checks write access on the base task and read access on the other task), the relation **display** path does not re-check permissions for the current reader. This means a privileged user can create a relation that then leaks data to all other users who can read the base task.\n\n## PoC\n\n**Setup:** Two users (User A, User B), two projects (Project-Shared, Project-Private).\n- User A has access to both projects.\n- User B has access only to Project-Shared.\n- Task 1 exists in Project-Shared, Task 2 exists in Project-Private.\n\n**Step 1: User A creates a relation between the two tasks**\n\n```bash\n# As User A (who has access to both projects)\ncurl -X PUT \"http://localhost:3456/api/v1/tasks/TASK1_ID/relations\" \\\n -H \"Authorization: Bearer USER_A_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"other_task_id\": TASK2_ID, \"relation_kind\": \"related\"}'\n```\n\nExpected: 201 Created (User A has write on Task 1, read on Task 2).\n\n**Step 2: User B reads tasks from the shared project**\n\n```bash\n# As User B (who has NO access to Project-Private)\ncurl \"http://localhost:3456/api/v1/projects/PROJECT_SHARED_ID/views/VIEW_ID/tasks\" \\\n -H \"Authorization: Bearer USER_B_TOKEN\"\n```\n\nExpected: Task 1 should be returned, but related_tasks should NOT include Task 2.\n\n**Actual result:** The response includes Task 1 with the `related_tasks` field containing the full Task 2 object, including its `title`, `description`, `due_date`, `priority`, `percent_done`, `project_id`, and other metadata — despite User B having no access to Project-Private.\n\n## Impact\n\n- **Information disclosure**: Any authenticated user can read the full metadata of tasks in projects they do not have access to, as long as a relation exists from a task they can read.\n- **Leaked fields include**: title, description, due dates, start dates, priority, percent completion, project ID, hex color, task index, done status, repeat configuration, cover image attachment ID, and creation/update timestamps.\n- **Project structure disclosure**: The `project_id` field reveals the existence and IDs of private projects.\n- **No user interaction required**: Once a privileged user creates a cross-project relation (which is intentionally allowed), the data leak is automatic for all readers of the base task.\n- **Blast radius**: Affects all Vikunja instances with cross-project task relations. In multi-tenant or team environments where projects have different access scopes, this undermines project-level access control.\n\n## Recommended Fix\n\nFilter related tasks by the requesting user's read permissions before adding them to the response. In `addRelatedTasksToTasks()`, after fetching full task objects, check that the user can read each related task's project:\n\n```go\nfunc addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task, a web.Auth) (err error) {\n relatedTasks := []*TaskRelation{}\n err = s.In(\"task_id\", taskIDs).Find(&relatedTasks)\n if err != nil {\n return\n }\n\n var relatedTaskIDs []int64\n for _, rt := range relatedTasks {\n relatedTaskIDs = append(relatedTaskIDs, rt.OtherTaskID)\n }\n\n if len(relatedTaskIDs) == 0 {\n return\n }\n\n fullRelatedTasks := make(map[int64]*Task)\n err = s.In(\"id\", relatedTaskIDs).Find(&fullRelatedTasks)\n if err != nil {\n return\n }\n\n // Filter related tasks by user's read permission\n allowedProjectIDs := make(map[int64]bool)\n checkedProjectIDs := make(map[int64]bool)\n for _, t := range fullRelatedTasks {\n if checkedProjectIDs[t.ProjectID] {\n continue\n }\n checkedProjectIDs[t.ProjectID] = true\n p := &Project{ID: t.ProjectID}\n canRead, _, err := p.CanRead(s, a)\n if err != nil {\n log.Errorf(\"Could not check project read permission: %v\", err)\n continue\n }\n if canRead {\n allowedProjectIDs[t.ProjectID] = true\n }\n }\n\n taskFavorites, err := getFavorites(s, relatedTaskIDs, a, FavoriteKindTask)\n if err != nil {\n return err\n }\n\n for _, rt := range relatedTasks {\n task, has := fullRelatedTasks[rt.OtherTaskID]\n if !has {\n continue\n }\n // Skip related tasks the user cannot access\n if !allowedProjectIDs[task.ProjectID] {\n continue\n }\n fullRelatedTasks[rt.OtherTaskID].IsFavorite = taskFavorites[rt.OtherTaskID]\n otherTask := &Task{}\n err = copier.Copy(otherTask, fullRelatedTasks[rt.OtherTaskID])\n if err != nil {\n log.Errorf(\"Could not duplicate task object: %v\", err)\n continue\n }\n otherTask.RelatedTasks = nil\n taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(\n taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], otherTask)\n }\n\n return\n}\n```\n\nThis checks project-level read permission once per unique project ID (cached in `allowedProjectIDs`) and skips related tasks from projects the user cannot access.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "code.vikunja.io/api"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "2.2.1"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 2.2.0"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-8cmm-j6c4-rr8v"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33676"
49+
},
50+
{
51+
"type": "WEB",
52+
"url": "https://github.com/go-vikunja/vikunja/pull/2449"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://github.com/go-vikunja/vikunja/commit/833f2aec006ac0f6643c41872e45dd79220b9174"
57+
},
58+
{
59+
"type": "PACKAGE",
60+
"url": "https://github.com/go-vikunja/vikunja"
61+
},
62+
{
63+
"type": "WEB",
64+
"url": "https://vikunja.io/changelog/vikunja-v2.2.2-was-released"
65+
}
66+
],
67+
"database_specific": {
68+
"cwe_ids": [
69+
"CWE-863"
70+
],
71+
"severity": "MODERATE",
72+
"github_reviewed": true,
73+
"github_reviewed_at": "2026-03-25T21:17:12Z",
74+
"nvd_published_at": "2026-03-24T16:16:34Z"
75+
}
76+
}

0 commit comments

Comments
 (0)