---
title: "GHSA-hgjx-r89m-m7v4: FacturaScripts: Path traversal in UploadedFile::move() via getClientOriginalName() — arbitrary file write outside MyFiles/ leading to   RCE"
url: https://erroneo.us/security/ghsa/GHSA-hgjx-r89m-m7v4/
description: "GHSA-hgjx-r89m-m7v4 (critical, Composer (PHP)): FacturaScripts: Path traversal in UploadedFile::move() via getClientOriginalName() — arbitrary file write…"
type: advisory
section: "Security Digest"
date_published: 2026-07-14
date_modified: 2026-07-14
publisher: erroneo.us
author: erroneo.us Security Desk
ghsa: "GHSA-hgjx-r89m-m7v4"
license: All rights reserved. Quote with attribution and a link.
---

# GHSA-hgjx-r89m-m7v4: FacturaScripts: Path traversal in UploadedFile::move() via getClientOriginalName() — arbitrary file write outside MyFiles/ leading to   RCE

## Affected packages

- Composer (PHP) `facturascripts/facturascripts` >= 2025, <= 2026.2 → fixed: none listed

## Details

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 nodemodules/ from the index.php rewrite, files written into those directories are served directly by Apache. Combined with .htaccess not being in BLOCKEDEXTENSIONS, 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 isuploadedfile() — 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) upload Representative 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. PoC Step 1 — Static reproduction of the file-write primitive The 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 request After 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…

## Sources

- [GitHub Advisory GHSA-hgjx-r89m-m7v4](https://github.com/advisories/GHSA-hgjx-r89m-m7v4)
- [github.com](https://github.com/NeoRazorX/facturascripts/security/advisories/GHSA-hgjx-r89m-m7v4)

---
Source page: https://erroneo.us/security/ghsa/GHSA-hgjx-r89m-m7v4/ · Security Digest: https://erroneo.us/security/ · More: https://erroneo.us/llms.txt
