Vikunja principal-type and cross-object authorization checks¶
Sources: hourly offensive-security scan, 2026-08-02 GitHub Security Advisory feed. Primary entries: GHSA-9jrx-vmh8-c6xw / CVE-2026-68581 and GHSA-p4r4-8cxw-pjx7 / CVE-2026-68582.
These advisories expose two reusable API-testing patterns: numeric IDs from different principal namespaces colliding behind a generic authentication interface, and an authorized parent object being combined with an independently selected child object that is loaded before authorization.
Authorized validation only
Use a disposable Vikunja instance, synthetic users, private test projects, canary tasks, and throwaway share links. Never enumerate a public instance, access another customer's projects, retain API tokens, or delete a real user's credentials.
Confirmed boundaries¶
| Advisory | Affected versions | Failed binding | Operator value |
|---|---|---|---|
| GHSA-9jrx-vmh8-c6xw | 0.22.0 through 2.3.0 | link-share IDs and user IDs occupy independent numeric sequences, but token-management routes consume a generic numeric principal ID without checking its type | Test every credential-management route with each accepted principal class, especially when IDs can collide. |
| GHSA-p4r4-8cxw-pjx7 | 0.24.0 through 2.3.0 | task scope remains pinned to the shared project while the path-selected view is loaded from another project without a matching authorization check | Test parent/child route selectors independently and preserve response-shape/existence-oracle evidence. |
Preconditions and inputs¶
- A disposable Vikunja deployment and a
2.4.0patched control. - Users A and B with synthetic profiles.
- Projects A and B, at least one view and kanban bucket in each, and marker-only tasks.
- One link share for project A. Additional shares may be created only in the lab to construct an intentional numeric-ID collision.
- An HTTP recorder that redacts JWTs and API-token values while preserving route, principal type, numeric ID, selected project/view, status, and response field names.
Principal-type collision matrix¶
- Record user B's numeric ID from the authorized lab user-search path. Do not enumerate unknown users or infer production ID ranges.
- In project A, create disposable link shares until one share ID equals B's user ID. This sequencing step is acceptable only in the local fixture; do not generate shares against a production service.
- Authenticate with the colliding link-share JWT and exercise the token route family in ascending impact order: list, create through a recorder/no-op sink, then delete through a recorder. Compare the same calls with user A, user B, a non-colliding share, and an expired share.
- For token creation, request only a harmless profile/status scope and intercept persistence where possible. Positive evidence is that the resolved token owner is user B even though the authenticated principal is a link share.
- For deletion, do not remove an actual token. A patched repository, database transaction rollback, or fake token-store recorder is sufficient to show which owner/token row would be selected.
- Repeat on
2.4.0, where link-share principals should be rejected from user API-token management regardless of numeric equality.
The report should prove link-share principal with ID N -> generic principal resolver treats N as user N -> user-token operation selected. ID equality alone is not an authorization bypass; include the route decision or intercepted side effect.
Project/view selector matrix¶
Create project A as the shared project and project B as private to user B. Give each a view and bucket with unique non-sensitive markers.
| Share principal | Path project | Path view | Expected result | Evidence to capture |
|---|---|---|---|---|
| share A | A | view A | allowed control | A task/bucket markers only |
| share A | A | view B | denied | status, response field names, no B marker beyond the canary needed for proof |
| share A | B | view B | denied | route and auth decision only |
| share A | A | unknown view | not found control | status/body-shape fingerprint |
- Request
GET /api/v1/projects/{project}/views/{view}/tasksfor each row and record the path IDs, share-bound project, loaded view owner, and response schema. - Confirm task rows remain scoped to A. The advisory's distinct impact is foreign bucket metadata and the full synthetic
created_byobject, not cross-project task content. - Compare known foreign, unknown, and unauthorized view IDs to determine whether status or body shape creates an existence oracle. Keep the candidate set to the fixture's known IDs.
- Repeat as users A and B and on
2.4.0. A patched result should authorize the view against the same principal/project context before returning bucket or creator fields.
Report share for project A -> path selects view from project B -> foreign synthetic bucket/creator fields returned, and report the existence oracle separately as known versus unknown view -> stable response differential. Do not inflate the finding into arbitrary task disclosure when the task scope remains constrained.
Evidence checklist¶
- Authenticated principal type and numeric ID are recorded separately.
- Parent project, child view, resolved owner, and output object types are shown.
- Token values and JWTs are redacted; only synthetic IDs and marker fields remain.
- Mutating token operations use a recorder, rollback, or disposable token.
- Non-colliding, unknown-ID, legitimate-user, and patched-version controls are included.
- Claims distinguish token-management authority, bucket metadata disclosure, and existence-oracle behavior.
August 28 follow-up: kanban cross-tenant mass-assignment and incomplete-fix detach¶
Advisories (2026-08-28 GitHub wave): GHSA-5pg6-m483-7vrg / CVE-2026-55066 (high), GHSA-gg93-x632-9ccv / CVE-2026-55065 (high), GHSA-569v-q83c-3j3g / CVE-2026-55067 (medium), GHSA-f27p-pw2p-9pr4 / CVE-2026-54766 (medium), GHSA-44v6-7fxq-vgf4 / CVE-2026-55064 (medium). All affect v2.3.0; the detach incomplete-fix and the kanban items are fixed in 2.4.0.
The 08-02 boundaries (principal-type collision, parent/child selector) are about which object gets loaded. This wave is about the write path accepting a caller-controlled foreign-key field that the URL-scoped authorization check never re-validates. The reusable pattern:
A route authorizes on the object in the URL path but
UPDATE/DELETEre-binds a caller-supplied FK from the request body (or a cascade that ignores the URL-scoped parent). The permission check passes on the URL object; the mutation lands on a foreign-tenant object.
| Advisory | Endpoint | Unchecked field | Result |
|---|---|---|---|
| GHSA-5pg6-m483-7vrg | POST /api/v1/projects/{project}/views/{view}/buckets/{bucket}/tasks | body task_id | Cross-tenant IDOR: victim task loaded in full and, into a "done" bucket, written — task IDs are a global sequential integer space |
| GHSA-gg93-x632-9ccv | DELETE /api/v1/projects/{project}/views/{view} | cascading task_buckets / task_positions deletes filter only on pv.ID | Instance-wide destruction of kanban bucket assignments + task ordering in any project view; CanDelete gates Admin on the URL project but never confirms the URL view belongs to it |
| GHSA-569v-q83c-3j3g | POST /api/v1/projects/{project}/views/{view}/buckets/{bucket} | body project_view_id | Cross-tenant kanban-bucket relocation: graft the attacker's bucket into any other tenant's kanban view with attacker-controlled title |
| GHSA-f27p-pw2p-9pr4 | project-duplication endpoint | ParentProjectID | CanCreate calls parent.CanCreate ("may I create this project?") instead of parent.CanWrite ("may I create children inside this project?") — any read-access user duplicates a project into any parent project |
| GHSA-44v6-7fxq-vgf4 | project update | parent_project_id: 0 | Incomplete fix for CVE-2026-35595: the Admin gate only fires for parent_project_id > 0; a Write-only user on a shared child detaches it to root (0), severing the recursive-CTE Admin inheritance |
Replayable validation (lab only)¶
Preconditions: disposable Vikunja 2.3.0 plus a 2.4.0 patched control; two synthetic users (attacker/victim) with their own projects, views, kanban buckets, and marker tasks; a redacting HTTP recorder.
- Bucket relocation (GHSA-569v-q83c-3j3g). Attacker creates a bucket in their own project (step 1 in the advisory PoC), then
POSTs an update to that bucket withproject_view_idset to the victim's view ID. The URL chain is the attacker's, soBucket.CanUpdatepasses; the body field writes through. Positive: the bucket row now carries the victim'sproject_view_id. On2.4.0the same call must be denied or the field must be ignored. - Cross-tenant task move (GHSA-5pg6-m483-7vrg). Attacker moves their own task into a bucket in their own project, but with the victim's
task_idin the body. Positive: the response returns the victim task's full contents and, when the target bucket is "done," the victim task row is mutated. Bound the claim to the exacttask_idbinding — do not enumerate the global ID space against production. - View-delete cascade (GHSA-gg93-x632-9ccv). With Admin on the attacker's project only,
DELETEa view whoseview_idbelongs to the victim's project but is addressed through the attacker's URL project. Positive:task_buckets/task_positionsrows for the victim view are deleted. Use marker rows and a rollback/recorder for the mutation evidence; the advisory's two-tenant fixture is sufficient. - Duplicate-into-foreign-parent (GHSA-f27p-pw2p-9pr4). As a user with read access to the victim project and no write access to the victim's parent, request duplication into that parent. Positive: a new project owned by the attacker appears under the foreign parent. Compare against the normal create path, which enforces
CanWrite. - Detach-to-root incomplete fix (GHSA-44v6-7fxq-vgf4). As a Write-only (not Admin) user on a shared child project, send a project update with
parent_project_id: 0. Positive: the child is detached from its parent and the recursive-CTE Admin inheritance is severed. Then re-run withparent_project_id: <foreign parent>to confirm the> 0case is gated, establishing the differential. On2.4.0both must be denied for a non-Admin.
Reporting heuristics¶
- For each item, report URL-scoped object (authorized) vs body/cascade FK (foreign-tenant) with the exact model method that re-binds the field (
Bucket.Update,ProjectView.Delete,ProjectDuplicate.CanCreate, project-update handler). - Capture the
2.4.0negative control for every item — several of these are regression fixes, and the "incomplete fix" framing (GHSA-44v6-7fxq-vgf4) is only provable by showing the> 0path is gated while= 0is not. - Distinguish read oracle (task contents returned) from write primitive (row mutated / rows deleted) — the write evidence is what makes this a high, not a medium.
- Kanban/task ordering corruption is a tenant-integrity finding, not just an authorization miss: report the blast radius (all views of one project vs. instance-wide) precisely.
Safe boundaries¶
- Disposable instance, synthetic users/projects/tasks only; no real user data, no production ID enumeration.
- Mutations go through marker rows with rollback or a recorder; the
2.4.0control run must show the denial before any claim is made. - Redact JWTs/tokens; evidence is route + field + status + resolved owner, not session material.