Medium severity4.3NVD Advisory· Published Apr 10, 2026· Updated Apr 17, 2026
CVE-2026-35598
CVE-2026-35598
Description
Vikunja is an open-source self-hosted task management platform. Prior to 2.3.0, the CalDAV GetResource and GetResourcesByList methods fetch tasks by UID from the database without verifying that the authenticated user has access to the task's project. Any authenticated CalDAV user who knows (or guesses) a task UID can read the full task data from any project on the instance. This vulnerability is fixed in 2.3.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
code.vikunja.io/apiGo | < 2.3.0 | 2.3.0 |
Affected products
1Patches
1879462d71735fix(caldav): enforce URL project match in GetResourcesByList
3 files changed · +108 −4
pkg/routes/caldav/listStorageProvider.go+25 −4 modified@@ -175,16 +175,32 @@ func principalPathForUser(username string) string { // GetResourcesByList fetches a list of resources from a slice of paths func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) (resources []data.Resource, err error) { - // Parse the set of resourcepaths into usable uids - // A path looks like this: /dav/projects/10/a6eb526d5748a5c499da202fe74f36ed1aea2aef.ics - // So we split the url in parts, take the last one and strip the ".ics" at the end + // Path format: /dav/projects/{projectID}/{uid}.ics. + // Remember the href's project ID per uid so the consistency check below + // can drop tasks requested via the wrong project (GHSA-48ch-p4gq-x46x). var uids []string + uidURLProjects := map[string]int64{} for _, path := range rpaths { parts := strings.Split(path, "/") if len(parts) < 5 { continue } - uids = append(uids, strings.TrimSuffix(parts[4], ".ics")) + // Skip malformed hrefs: without these guards an empty/non-numeric + // project would bypass the consistency check, and an empty UID + // would query for tasks with empty/NULL uids. + if !strings.HasSuffix(parts[4], ".ics") { + continue + } + uid := strings.TrimSuffix(parts[4], ".ics") + if uid == "" { + continue + } + urlProjectID, perr := strconv.ParseInt(parts[3], 10, 64) + if perr != nil { + continue + } + uids = append(uids, uid) + uidURLProjects[uid] = urlProjectID } if len(uids) == 0 { @@ -207,6 +223,11 @@ func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) (re } for _, t := range tasks { + // Closes the URL-path leak for calendar-multiget REPORTs + // (GHSA-48ch-p4gq-x46x). + if urlProjectID, ok := uidURLProjects[t.UID]; ok && urlProjectID != t.ProjectID { + continue + } rr := VikunjaProjectResourceAdapter{ task: t, }
pkg/routes/caldav/listStorageProvider_test.go+47 −0 modified@@ -486,3 +486,50 @@ END:VCALENDAR` assert.Equal(t, "uid-caldav-test-child-task-2", formerSiblingSubTask.UID) }) } + +// TestGetResourcesByList_URLProjectConsistency covers GHSA-48ch-p4gq-x46x. +func TestGetResourcesByList_URLProjectConsistency(t *testing.T) { + u := &user.User{ + ID: 15, + Username: "user15", + } + + t.Run("drops task when href project does not match task project", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + + storage := &VikunjaCaldavProjectStorage{user: u} + + // uid-caldav-test lives in project 36 (fixtures/tasks.yml id 40). + resources, err := storage.GetResourcesByList([]string{ + "/dav/projects/38/uid-caldav-test.ics", + }) + require.NoError(t, err) + assert.Empty(t, resources, "task must not be returned when href project mismatches") + }) + + t.Run("returns task when href project matches task project", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + + storage := &VikunjaCaldavProjectStorage{user: u} + + resources, err := storage.GetResourcesByList([]string{ + "/dav/projects/36/uid-caldav-test.ics", + }) + require.NoError(t, err) + require.Len(t, resources, 1) + }) + + t.Run("drops task for unauthorized user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + + // user 6 has no access to project 36 + outsider := &user.User{ID: 6, Username: "user6"} + storage := &VikunjaCaldavProjectStorage{user: outsider} + + resources, err := storage.GetResourcesByList([]string{ + "/dav/projects/36/uid-caldav-test.ics", + }) + require.NoError(t, err) + assert.Empty(t, resources, "unauthorized user must not receive any tasks") + }) +}
pkg/webtests/caldav_test.go+36 −0 modified@@ -107,6 +107,42 @@ END:VCALENDAR` assert.NotContains(t, rec.Body.String(), "Title Caldav Test") assert.Equal(t, http.StatusNotFound, rec.Result().StatusCode) }) + t.Run("Multiget rejects UIDs from other projects", func(t *testing.T) { + e, _ := setupTestEnv() + + // href claims project 38, but uid-caldav-test lives in project 36. + reportBody := `<?xml version="1.0" encoding="utf-8" ?> +<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> + <D:prop> + <D:getetag/> + <C:calendar-data/> + </D:prop> + <D:href>/dav/projects/38/uid-caldav-test.ics</D:href> +</C:calendar-multiget>` + + rec, err := newCaldavTestRequestWithUser(t, e, "REPORT", caldav.ProjectHandler, &testuser15, reportBody, nil, map[string]string{"project": "38"}) + require.NoError(t, err) + assert.NotContains(t, rec.Body.String(), "Title Caldav Test") + assert.NotContains(t, rec.Body.String(), "Description Caldav Test") + }) + t.Run("Multiget cross-user UID read is rejected", func(t *testing.T) { + e, _ := setupTestEnv() + + reportBody := `<?xml version="1.0" encoding="utf-8" ?> +<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> + <D:prop> + <D:getetag/> + <C:calendar-data/> + </D:prop> + <D:href>/dav/projects/36/uid-caldav-test.ics</D:href> +</C:calendar-multiget>` + + // testuser6 cannot access project 36 where uid-caldav-test lives. + rec, err := newCaldavTestRequestWithUser(t, e, "REPORT", caldav.ProjectHandler, &testuser6, reportBody, nil, map[string]string{"project": "36"}) + require.NoError(t, err) + assert.NotContains(t, rec.Body.String(), "Title Caldav Test") + assert.NotContains(t, rec.Body.String(), "Description Caldav Test") + }) } func TestCaldavDiscovery(t *testing.T) {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/go-vikunja/vikunja/commit/879462d717351fe5d276ddec5246bdec31b41661nvdPatchWEB
- github.com/go-vikunja/vikunja/security/advisories/GHSA-48ch-p4gq-x46xnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-48ch-p4gq-x46xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-35598ghsaADVISORY
- github.com/go-vikunja/vikunja/pull/2579nvdIssue TrackingWEB
- github.com/go-vikunja/vikunja/releases/tag/v2.3.0nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.