{"id":"CVE-2026-94619","aliases":["GHSA-44r8-3p76-84mw"],"url":"https://o3.security/vulnerability/CVE-2026-94619","summary":"FileGator: Missing authorization in POST /upload allows cross-user read and deletion of temporary files","details":"## Summary\n\n`POST /upload` uses the request-supplied `resumableFilename` directly as a key into the shared temporary directory, without the character filter it applies to `resumableIdentifier` and without namespacing it per user. A valid user holding the `upload` permission can read any file in that directory into their own home directory, append to it, and delete it.\n\nThis reproduces the cross-user batch-archive disclosure of GHSA-f74m-x83r-c4v4 through a different route — it still works with the fix for that advisory (PR #597) applied — and additionally allows deleting the login rate-limit lockfile.\n\nReported with explicit preconditions and bounded impact, as before: the primitive is confined to the temporary directory, not the whole filesystem.\n\n## Details\n\n- `backend/Controllers/UploadController.php:61` — `$file_name = $request->input('resumableFilename', 'file')` is **not** passed through the `preg_replace('/[^0-9a-zA-Z_]/', '', ...)` that line 66 applies to `resumableIdentifier`.\n- Line 88 builds `$prefix` namespaced by the sanitized username, but `$file_name` reaches the temporary storage at lines 118, 121 and 125 **without** that prefix.\n- Line 115 — `if ($chunks_size >= $total_size)` is entered whenever `resumableTotalSize` is `0`.\n- Lines 116-119 — the assembly loop `for ($i = 1; $i <= $total_chunks; ++$i)` does not execute when `resumableTotalChunks` is `0`, so nothing is written to `$file_name`.\n- Line 121 — `readStream($file_name)` therefore reads the pre-existing temporary file of that exact name.\n- Line 122 — its contents are stored into the requester's own home directory.\n- Line 125 — `remove($file_name)` deletes it.\n\nPath traversal outside the temporary directory is prevented by `Tmpfs::sanitizeFilename()`, so the primitive is bounded to that directory — but every user's transient objects share it.\n\n## PoC\n\nPreconditions: a multi-user deployment with distinct per-user `homedir`s. The attacker is a valid low-privileged user holding `upload` (a first-class permission in the admin UI, and present in the shipped `users.json.blank` admin set).\n\n1. Victim creates a batch archive via `POST /batchdownload`; the JSON response returns the archive id.\n2. Attacker sends **one** `POST /upload` with `resumableFilename=<victim's id>`, `resumableTotalChunks=0`, `resumableTotalSize=0`, `resumableRelativePath=/`, and any small valid file part.\n3. Response is `{\"data\":\"Stored\"}`. The victim's archive is now byte-identical in the attacker's home directory, retrievable through the ordinary `GET /download`. The victim's temporary archive has been deleted.\n\nVerified end to end with a PHPUnit test against current `master`; the archive contents matched byte for byte. Test file available on request.\n\n## Impact\n\nMissing authorization (CWE-862) on temporary storage objects.\n\n- **Cross-user disclosure** of any transient object whose name the attacker can learn or predict. This includes batch archives — so GHSA-f74m-x83r-c4v4's impact remains reachable even with its fix applied — and the plaintext per-file copies that `ZipArchiver::addFileFromStorage()` writes while an archive is being built (those still use `uniqid()`, which is timestamp-derived).\n- **Arbitrary delete** within the temporary directory. `AuthController::login()` stores the login rate-limit counter as `md5($ip).'.lock'` in that same directory (`backend/Controllers/AuthController.php:36`, checked at line 44, written at line 58). An attacker can compute their own lockfile name and delete it, defeating the lockout.\n- **Arbitrary append**: with `resumableTotalChunks >= 1`, line 118 `write($file_name, ..., true)` appends attacker-controlled bytes to an arbitrary existing temporary file.\n\n## Remediation\n\nDo not use the request-supplied filename as a storage key. Namespace the assembly file per user — for example reuse the existing `$prefix` — and carry the display filename separately for the `storage->store()` call.\n\nNote that simply applying the `[^0-9a-zA-Z_]` filter to `resumableFilename` would corrupt legitimate uploads (`sample.txt` becomes `sampletxt`), because `$final['filename']` is what becomes the stored name. The fix needs the namespacing, not just the filter.","published":"2026-11-06T21:32:00.424Z","modified":"2026-11-10T02:30:51.669812487Z","cvss":{"score":5.9,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:L/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/cae442a8323fddae7c8cee0924cdbb1aa6eea702","label":"filegator/filegator@cae442a"},"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-94619.json"},{"type":"ADVISORY","url":"https://github.com/filegator/filegator/security/advisories/GHSA-44r8-3p76-84mw"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-94619"},{"type":"FIX","url":"https://github.com/filegator/filegator/commit/cae442a8323fddae7c8cee0924cdbb1aa6eea702"},{"type":"FIX","url":"https://github.com/filegator/filegator/pull/598"},{"type":"PACKAGE","url":"https://github.com/filegator/filegator"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-11-10T02:30:51.669812487Z"}}