{"id":"CVE-2026-94618","aliases":["GHSA-f74m-x83r-c4v4"],"url":"https://o3.security/vulnerability/CVE-2026-94618","summary":"FileGator: Missing authorization in GET /batchdownload allows cross-user retrieval of another user's archive","details":"### Summary\n`GET /batchdownload` streams a batch ZIP to any requester who supplies its `uniqid`, without checking that the current user/session created it. In deployments where users have separate home directories, a valid low-privileged user who obtains an active archive id can download another user's batch archive, disclosing files the per-user boundary otherwise protects. Reported with explicit preconditions and bounded impact — not a universal \"any user steals any file\" issue.\n\n### Details\nThe archive id is generated with plain `uniqid()` and no ownership metadata is bound to it before streaming.\n\n- `backend/Services/Archiver/Adapters/ZipArchiver.php:44` — `createArchive()` uses plain `uniqid()`.\n- `backend/Controllers/DownloadController.php:144-145` — `batchDownloadStart()` reads the tmpfs file by the request-supplied `uniqid` after only a charset `preg_replace()`.\n- `backend/Controllers/DownloadController.php:158` — the tmpfs archive is removed only after streaming.\n- `backend/Controllers/routes.php:207-226` — both batch endpoints are exposed to normal roles with `read`, `download`, `batchdownload`.\n\nNo user id, session id, home directory, or creator check is applied before streaming. The code path is unchanged since commit `3f03a40` (2022-05-31) and is present in the latest release `v7.14.3`.\n\n### PoC\nPreconditions: a multi-user deployment with distinct per-user `homedir`s (set per-user in `users.json`, or `private_repos => true` on the WordPress/LDAP adapter). The shipped sample config (`admin`/`guest` both at `/`, `guest` with no perms) does not create this boundary on its own. Attacker is a valid user with `read`, `download`, `batchdownload`.\n\n1. User John (`homedir /john`) creates `secret.txt`, signs in, and creates a batch archive via `POST /batchdownload`. The JSON response returns the archive `uniqid`.\n2. User Jane (`homedir /jane`, with `batchdownload`) signs in normally.\n3. Jane's direct `GET /download` of `/secret.txt` returns `302` — the normal home-directory boundary blocks the file.\n4. Jane sends `GET /batchdownload?uniqid=<John's id>` and receives a valid ZIP containing `secret.txt` with John's content.\n\nA PHPUnit proof with two real sessions reproduces this end to end (`OK, 3 tests / 29 assertions`), plus a lifecycle test (`OK, 2 tests / 9 assertions`). Full test files, run logs, and a real Apache access-log replay demo are available on request.\n\nLifetime / acquisition: a normal completed download removes the archive at the end of streaming, limiting the window. An aborted/failed download leaves the archive in tmpfs (cleanup never reached). The frontend issues the download as a top-level navigation (`window.open('.../batchdownload&uniqid='+ret.uniqid, '_blank')`), so the id appears in standard web access logs. That access-log path only adds meaningful impact when log visibility is decoupled from host filesystem access (e.g. a log-aggregation/shared-hosting/downstream-sink reader) and the archive is still alive; if the reader is ops/root on the same host they can already read the storage tree directly. The `uniqid()` token is timestamp-derived (1,000,000-value space per known second) — included to justify a CSPRNG, not presented as a practical universal brute force.\n\n### Impact\nMissing authorization (CWE-862) on a temporary download object, leading to cross-user file disclosure across the per-user home-directory boundary in configured multi-user deployments. Secondary effect: a successful replay deletes the victim's pending archive (the GET removes the tmpfs file after streaming), denying the victim their own download if not yet consumed.\n\nRemediation: generate ids with `bin2hex(random_bytes(32))`; store creator user/session + home dir + expiry per archive id; verify the requester matches before streaming and return `403`/`404` on mismatch or unknown/expired id; delete metadata with the tmpfs file.","published":"2026-11-06T21:30:39.923Z","modified":"2026-11-08T02:47:43.513826744Z","cvss":{"score":5.4,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:N/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"filegator/filegator","fixedVersion":"7.15.0"}],"fix":{"url":"https://github.com/filegator/filegator/commit/638737e560cbf20cbeb5da4e4e6c974f8a080531","label":"filegator/filegator@638737e"},"references":[{"type":"WEB","url":"https://github.com/filegator/filegator/releases/tag/v7.15.0"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/94xxx/CVE-2026-94618.json"},{"type":"ADVISORY","url":"https://github.com/filegator/filegator/security/advisories/GHSA-f74m-x83r-c4v4"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-94618"},{"type":"FIX","url":"https://github.com/filegator/filegator/commit/638737e560cbf20cbeb5da4e4e6c974f8a080531"},{"type":"FIX","url":"https://github.com/filegator/filegator/pull/597"},{"type":"PACKAGE","url":"https://github.com/filegator/filegator"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-11-08T02:47:43.513826744Z"}}