GHSA-hgjx-r89m-m7v4 FacturaScripts: Path traversal in UploadedFile::move() via getClientOriginalName() — arbitrary file write outside MyFiles/ leading to   RCE
Summary`FacturaScripts\Core\UploadedFile::move($destiny, $destinyName)` concatenates `$destiny` and `$destinyName` without normalizing the resulting path. Every caller in the codebase passes `UploadedFile::getClientOriginalName()` — the unsanitized client-supplied filename — as `$destinyName`, so an authenticated user submitting a filename containing `../` segments can write the uploaded content to any directory writable by the web-server user, escaping the intended `MyFiles/` location.Because the shipped `htaccess-sample` (the documented production Apache configuration) excludes `Dinamic/Assets/` and `node_modules/` from the `index.php` rewrite, files written into those directories are served directly by Apache. Combined with `.htaccess` not being in `BLOCKED_EXTENSIONS`, the primitive escalates from arbitrary file write to remote code execution.Vulnerable Code`Core/UploadedFile.php`:`isValid()` only checks the extension blocklist, the upload error code, and `is_uploaded_file()` — it never inspects the filename for directory separators or `..` segments.Six call sites pass the raw client filename straight into `move()`:`Core/Controller/ApiUploadFiles.php:58` — `POST /api/3/uploadfiles``Core/Controller/ApiAttachedFiles.php:136` — `POST /api/3/attachedfiles``Core/Lib/Widget/WidgetFile.php:84` — every form using a file widget`Core/Lib/Widget/WidgetLibrary.php:215` — library widget upload`Core/Lib/ExtendedController/DocFilesTrait.php:51` — document files trait`Core/Controller/AdminPlugins.php:260` — plugin (zip) uploadRepresentative sink — `Core/Controller/ApiUploadFiles.php:56-79`:Shipped `htaccess-sample` (production Apache rules):Apache therefore serves any file under `Dinamic/Assets/` directly, bypassing `index.php` entirely.PoCStep 1 — Static reproduction of the file-write primitiveThe following script replicates `UploadedFile::move()`'s `rename()` path verbatim inside a sandboxed temp directory. It does not run any payload — it only demonstrates that the destination escapes `MyFiles/` when the filename contains `../`.Output:Step 2 — Equivalent live HTTP requestAfter the request, `Dinamic/Assets/traversed.txt` exists on disk and is reachable at `https://target/Dinamic/Assets/traversed.txt` — Apache serves it directly because the path is excluded from the `index.php` rewrite.Step 3 — Chain to code executionBecause `.htaccess` is not in `BLOCKED_EXTENSIONS`, the same primitive can write an Apache override into `Dinamic/Assets/`:Upload with filename `../Dinamic/Assets/.htaccess` and body `AddType application/x-httpd-php .png`Upload with filename `../Dinamic/Assets/x.png` containing a PHP payload (extension `png` is not blocked, content is not validated by `isValid()`)Request `https://target/Dinamic/Assets/x.png` — Apache hands it to the PHP handler per the uploaded `.htaccess`Root Cause`UploadedFile::move()` performs raw `$destiny . $destinyName` concatenation and trusts `getClientOriginalName()`, which returns `$this->name ?? ''` with no normalization. No call site applies `basename()` or any equivalent before passing the client filename to `move()`. The blocklist in `BLOCKED_EXTENSIONS` covers only PHP-family extensions and does not cover `htaccess`, which is required for the rewrite-excluded directory to be useful for code execution.ImpactAuthenticated attacker (any role with permission to call one of the six upload entry points — including any user allowed to attach a file to a record, or any API token with `uploadfiles`/`attachedfiles` access) can:Write arbitrary content to any path under the application root that is writable by the web-server user, including `Dinamic/Assets/` (Apache-direct-served) and `node_modules/`.Overwrite shipped JS/CSS inside `Dinamic/Assets/`, injecting client-side script that executes in every administrator's browser → session takeover on next admin page load.Drop a `.htaccess` into `Dinamic/Assets/` remapping a benign extension to the PHP handler, followed by a second upload that lands an executable payload — full remote code execution as the web-server user.The required precondition is only an authenticated session or API token with upload privileges, which is granted to a wide range of non-administrative roles in standard installations.FixMinimal fix — sanitize inside `UploadedFile::move()` so every call site is covered automatically:Apply the same change in `moveTo()`.Recommended hardening in addition:Add `htaccess`, `htm`, `html`, `shtml`, `phtm` to `BLOCKED_EXTENSIONS`, or replace the blocklist with an allowlist resolved per call site.After concatenating the final destination, verify with `realpath()` that the result is still inside the intended base directory; abort otherwise.Drop a `Deny from all` `.htaccess` (or equivalent web-server rule) into `MyFiles/` so even successfully written files cannot be requested directly without going through the application download endpoint (which already enforces `MyFilesToken`).StatusReported privately to the maintainer via GitHub Security Advisory. Awaiting acknowledgement.
CVSS v3.1 score: 9.9