FacturaScripts: Authenticated SQL injection in the FacturaScripts REST API filter parameter via parenthesis bypass in Where::sqlColumn
> Live PoC verified 2026-04-30 against a stock FacturaScripts master at 127.0.0.1:8081. A scoped ApiKey with fullaccess=0 and an ApiAccess row granting allowget=1 on the clientes resource only (no other rights, no UI session, no admin) issued one GET /api/3/clientes?filter[(0)UNION%20SELECT%20...]= request and the response body contained the raw bcrypt hash of the admin user's password ($2y$12$sLfA/XCqnjqLmYJwK.2V7eUHrHTHcQfkTYYfs1.lxX3OHrsmmkMGO) and the admin's logkey cookie value. The leaked logkey was injected into a fresh cookie jar and GET /AdminPlugins returned 200 with the admin plugin management UI. End-to-end account takeover from a read-only token with no CSRF, no second factor, no rate-limit interaction beyond the default 5-incident IP throttle.
Core/Where.php::sqlColumn() exempts any field name that contains both ( and ) from identifier escaping. The two API filter builders (APIModel::getWhereValues and ApiAttachedFiles::getWhereValues) feed the raw request key ($_GET['filter'][$key]) straight into new DataBaseWhere($key, $value, '=', ...). When the model's all() reaches Where::multiSqlLegacy -> Where::sql() -> Where::sqlColumn($key), the parenthesis branch returns the attacker-controlled string unmodified. The string is concatenated into WHERE <attacker> = '<value>', which an attacker can pivot to WHERE (0)UNION SELECT ... FROM users WHERE(nick='admin')-- = 'value', leaking arbitrary columns from any table.
Core/Lib/API/APIModel.php:300-322 (listAll):
```php
protected function listAll(): bool
{
$filter = $this->request->query->getArray('filter');
$limit = $this->request->query->getInt('limit', 50);
$offset = $this->request->query->getInt('offset', 0);
$operation = $this->request->query->getArray('operation');
$order = $this->request->query->getArray('sort');
// obtenemos los registros
$data = [];
$hidden = $this->model->getApiFieldsToHide();
$where = $this->getWhereValues($filter, $operation);
foreach ($this->model->all($where, $order, $offset, $limit) as $item) {
$data[] = $this->filterHidden($item->toArray(true), $hidden);
}
...
```
Core/Lib/API/APIModel.php:231-298 (getWhereValues):
```php
private function getWhereValues($filter, $operation, $defaultOperation = 'AND'): array
{
$where = [];
foreach ($filter as $key => $value) {
$field = $key; // (1) raw request key
$operator = '=';
switch (substr($key, -3)) { // suffix routing only
case '_gt': $field = substr($key, 0, -3); $operator = '>'; break;
case '_is': $field = substr($key, 0, -3); $operator = 'IS'; break;
case '_lt': $field = substr($key, 0, -3); $operator = '<'; break;
}
...
if (!isset($operation[$key])) {
$operation[$key] = $defaultOperation;
}
$where[] = new DataBaseWhere($field, $value, $operator, $operation[$key]); // (2)
}
return $where;
}
```
The function only ever reads the suffix to decide an operator. The remaining identifier - up to 252 characters in MariaDB and unrestricted by the framework - is preserved verbatim and handed to DataBaseWhere. There is no allow-list of legal column names, no preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/') like the autocomplete hardening in BaseController::autocompleteAction (commit b8aa78b), and no plug-in hook through which the operator could intervene.
The exact same code (line-for-line, plus a files parameter) lives in Core/Controller/ApiAttachedFiles.php::getWhereValues (lines 172-239), so the bug is present on both the generic /api/3/<resource> route and the dedicated /api/3/attachedfiles route.
DataBaseWhere::getSQLWhere now delegates to Where::multiSqlLegacyCore/Base/DataBase/DataBaseWhere.php is marked @deprecated and the active code path runs through Core/Where.php::multiSqlLegacy (lines 151-199), which converts each legacy DataBaseWhere instance to a Where:
```php
if ($item instanceof DataBaseWhere) {
$dbWhere = new self($item->fields, $item->value, $item->operator, $item->operation, $item->useField ?? false);
...
$sql .= $dbWhere->sql();
...
}
```
Where::sql() (lines 316-403) finally calls self::sqlColumn($field) for the identifier in every operator branch, including the = branch the attacker reaches.
Where::sqlColumn returns parenthesised inputs untouchedCore/Where.php:407-425:
```php
private static function sqlColumn(string $field): string
{
// si lleva paréntesis, no escapamos
if (strpos($field, '(') !== false && strpos($field, ')') !== false) {
return $field;
This issue can be reached over the network, attack complexity is low, an attacker needs low-level privileges on the target. No user interaction is required. The scope is changed, meaning a successful attack can affect components beyond the vulnerable one. Rated impact: confidentiality high, integrity high, availability high.
The score comes from this vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
CVE-2026-45262 is classified as CWE-89: SQL Injection. Untrusted input is concatenated into an SQL statement, letting an attacker change the query and reach data the request should not return.
CVE-2026-45262 is recorded against 1 package.
Published on 14 July 2026. No public exploit is currently recorded for this entry. Record sourced from OSV.
github.com (Web)
github.com (Package)
facturascripts/facturascripts has other advisories on record. If you are patching this one, these are worth checking on the same host:
These advisories are the same class of weakness (CWE-89: SQL Injection) in other software:
Details
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| facturascripts/facturascripts | — | — |
References
Similar Threats
Exploit Protection
CVE-2026-45262 carries CVSS 9.5 Critical rating. BotEraser checks your installation against this and other known CVE records, and blocks IPs associated with exploit activity.
Check My Site For CVE-2026-45262 →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.
Stay up to date with the latest from Boteraser.
We use cookies to improve your experience on our site. By using our site, you consent to cookies.
Manage your cookie preferences below:
Essential cookies enable basic functions and are necessary for the proper function of the website.
CloudFlare provides web performance and security solutions, enhancing site speed and protecting against threats.
Service URL: developers.cloudflare.com (opens in a new window)
These cookies are needed for adding comments on this website.
These cookies are used for managing login functionality on this website.
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Service URL: policies.google.com (opens in a new window)
You can find more information in our Cookie Policy and Privacy Policy.