From 5c4805b8225df1318758f6587f9e96aca35504d0 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Fri, 24 Jul 2026 18:12:45 -0400 Subject: [PATCH] Neutralize CSV formula injection in generated exports using the new created escapeCsvFormula Function --- admin/post/users.php | 2 +- agent/post/asset.php | 4 ++-- agent/post/certificate.php | 2 +- agent/post/client.php | 2 +- agent/post/contact.php | 2 +- agent/post/credential.php | 2 +- agent/post/domain.php | 2 +- agent/post/expense.php | 2 +- agent/post/invoice.php | 2 +- agent/post/location.php | 2 +- agent/post/network.php | 2 +- agent/post/payment.php | 2 +- agent/post/product.php | 2 +- agent/post/quote.php | 2 +- agent/post/recurring_invoice.php | 2 +- agent/post/software.php | 2 +- agent/post/ticket.php | 2 +- agent/post/transaction.php | 2 +- agent/post/trip.php | 2 +- agent/post/vendor.php | 2 +- functions/sanitize.php | 29 +++++++++++++++++++++++++++-- 21 files changed, 48 insertions(+), 23 deletions(-) diff --git a/admin/post/users.php b/admin/post/users.php index 5535c113..3630a547 100644 --- a/admin/post/users.php +++ b/admin/post/users.php @@ -326,7 +326,7 @@ if (isset($_POST['export_users_csv'])) { } $lineData = array($row['user_name'], $row['user_email'], $row['role_name'], $user_status_display, $row['user_created_at']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/asset.php b/agent/post/asset.php index 35b7b1dc..1f8056da 100644 --- a/agent/post/asset.php +++ b/agent/post/asset.php @@ -1229,7 +1229,7 @@ if (isset($_POST['export_assets_csv'])) { //output each row of the data, format line as csv and write to file pointer while ($row = mysqli_fetch_assoc($sql)) { $lineData = array($row['asset_name'], $row['asset_description'], $row['asset_type'], $row['asset_make'], $row['asset_model'], $row['asset_serial'], $row['asset_os'], $row['asset_purchase_date'], $row['asset_warranty_expire'], $row['asset_install_date'], $row['contact_name'], $row['location_name'], $row['asset_physical_location'], $row['asset_notes']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file @@ -1871,7 +1871,7 @@ if (isset($_POST['export_client_asset_interfaces_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = mysqli_fetch_assoc($sql)) { $lineData = array($row['interface_name'], $row['interface_description'], $row['interface_type'], $row['interface_mac'], $row['interface_ip'], $row['interface_nat_ip'], $row['interface_ipv6'], $row['network_name']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/certificate.php b/agent/post/certificate.php index 35ecaccf..ff43de27 100644 --- a/agent/post/certificate.php +++ b/agent/post/certificate.php @@ -376,7 +376,7 @@ if (isset($_POST['export_certificates_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()) { $lineData = array($row['certificate_name'], $row['certificate_description'], $row['certificate_domain'], $row['certificate_issued_by'], $row['certificate_expire']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/client.php b/agent/post/client.php index 5f24203b..00a0050f 100644 --- a/agent/post/client.php +++ b/agent/post/client.php @@ -514,7 +514,7 @@ if (isset($_POST['export_clients_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()) { $lineData = array($row['client_name'], $row['client_type'], $row['client_referral'], $row['client_website'], $row['location_name'], formatPhoneNumber($row['location_phone']), $row['location_address'], $row['location_city'], $row['location_state'], $row['location_zip'], $row['location_country'], $row['contact_name'], $row['contact_title'], formatPhoneNumber($row['contact_phone']), $row['contact_extension'], formatPhoneNumber($row['contact_mobile']), $row['contact_email'], $row['client_rate'], $row['client_currency_code'], $row['client_net_terms'], $row['client_tax_id_number'], $row['client_abbreviation']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/contact.php b/agent/post/contact.php index 7c14f000..a83d503f 100644 --- a/agent/post/contact.php +++ b/agent/post/contact.php @@ -1299,7 +1299,7 @@ if (isset($_POST['export_contacts_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()) { $lineData = array($row['contact_name'], $row['contact_title'], $row['contact_department'], $row['contact_email'], formatPhoneNumber($row['contact_phone']), $row['contact_extension'], formatPhoneNumber($row['contact_mobile']), $row['location_name']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/credential.php b/agent/post/credential.php index 629623a1..6011abc6 100644 --- a/agent/post/credential.php +++ b/agent/post/credential.php @@ -450,7 +450,7 @@ if (isset($_POST['export_credentials_csv'])) { $credential_username = decryptCredentialEntry($row['credential_username']); $credential_password = decryptCredentialEntry($row['credential_password']); $lineData = array($row['credential_name'], $row['credential_description'], $credential_username, $credential_password, $row['credential_otp_secret'], $row['credential_uri']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/domain.php b/agent/post/domain.php index d628545c..9965d8d6 100644 --- a/agent/post/domain.php +++ b/agent/post/domain.php @@ -493,7 +493,7 @@ if (isset($_POST['export_domains_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()) { $lineData = array($row['domain_name'], $row['domain_description'], $row['domain_registrar'], $row['domain_webhost'], $row['domain_expire']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/expense.php b/agent/post/expense.php index 34ef6279..5b52c7e0 100644 --- a/agent/post/expense.php +++ b/agent/post/expense.php @@ -373,7 +373,7 @@ if (isset($_POST['export_expenses_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = mysqli_fetch_assoc($sql)) { $lineData = array($row['expense_date'], $row['expense_amount'], $row['vendor_name'], $row['expense_description'], $row['category_name'], $row['account_name']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/invoice.php b/agent/post/invoice.php index e7aad18c..cadb3b98 100644 --- a/agent/post/invoice.php +++ b/agent/post/invoice.php @@ -698,7 +698,7 @@ if (isset($_POST['export_invoices_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()) { $lineData = array($row['invoice_prefix'] . $row['invoice_number'], $row['invoice_scope'], $row['invoice_amount'], $row['invoice_date'], $row['invoice_due'], $row['invoice_status'], $row['client_name']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/location.php b/agent/post/location.php index 1a9fb270..fc01514a 100644 --- a/agent/post/location.php +++ b/agent/post/location.php @@ -426,7 +426,7 @@ if(isset($_POST['export_locations_csv'])){ //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()){ $lineData = array($row['location_name'], $row['location_description'], $row['location_address'], $row['location_city'], $row['location_state'], $row['location_zip'], $row['location_phone'], $row['location_hours']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/network.php b/agent/post/network.php index 176e1a58..624ddc2b 100644 --- a/agent/post/network.php +++ b/agent/post/network.php @@ -210,7 +210,7 @@ if (isset($_POST['export_networks_csv'])) { //output each row of the data, format line as csv and write to file pointer while ($row = $sql->fetch_assoc()) { $lineData = array($row['network_name'], $row['network_description'], $row['network_vlan'], $row['network'], $row['network_gateway'], $row['network_dhcp_range'], $row['network_primary_dns'], $row['network_secondary_dns']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/payment.php b/agent/post/payment.php index fe1a82cc..590594e4 100644 --- a/agent/post/payment.php +++ b/agent/post/payment.php @@ -736,7 +736,7 @@ if (isset($_POST['export_payments_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()){ $lineData = array($row['payment_date'], $row['invoice_date'], $row['invoice_prefix'] . $row['invoice_number'], $row['invoice_amount'], $row['payment_amount'], $row['payment_method'], $row['payment_reference']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/product.php b/agent/post/product.php index 1297d80e..ef40a997 100644 --- a/agent/post/product.php +++ b/agent/post/product.php @@ -274,7 +274,7 @@ if (isset($_POST['export_products_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = mysqli_fetch_assoc($sql)) { $lineData = array($row['product_name'], $row['product_description'], $row['product_price'], $row['product_currency_code'], $row['category_name'], $row['tax_name']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/quote.php b/agent/post/quote.php index bf574e9b..cf982b8f 100644 --- a/agent/post/quote.php +++ b/agent/post/quote.php @@ -724,7 +724,7 @@ if(isset($_POST['export_quotes_csv'])){ //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()){ $lineData = array($row['quote_prefix'] . $row['quote_number'], $row['quote_scope'], $row['quote_amount'], $row['quote_date'], $row['quote_status']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/recurring_invoice.php b/agent/post/recurring_invoice.php index 084a82cf..4d3d81cf 100644 --- a/agent/post/recurring_invoice.php +++ b/agent/post/recurring_invoice.php @@ -639,7 +639,7 @@ if (isset($_POST['export_client_recurring_invoice_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()) { $lineData = array($row['recurring_invoice_prefix'] . $row['recurring_invoice_number'], $row['recurring_invoice_scope'], $row['recurring_invoice_amount'], ucwords($row['recurring_invoice_frequency'] . "ly"), $row['recurring_invoice_created_at']); - fputcsv($f, $lineData, $delimiter); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter); } //move back to beginning of file diff --git a/agent/post/software.php b/agent/post/software.php index c0dc937c..36c22d75 100644 --- a/agent/post/software.php +++ b/agent/post/software.php @@ -290,7 +290,7 @@ if (isset($_POST['export_software_csv'])) { } $lineData = array($row['software_name'], $row['software_version'], $row['software_description'], $row['software_type'], $row['software_license_type'], $row['software_seats'], $row['software_key'], $assigned_to_assets, $assigned_to_contacts, $row['software_purchase'], $row['software_expire'], $row['software_notes']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/ticket.php b/agent/post/ticket.php index ad9ad8d2..33834d08 100644 --- a/agent/post/ticket.php +++ b/agent/post/ticket.php @@ -2586,7 +2586,7 @@ if (isset($_POST['export_tickets_csv'])) { //output each row of the data, format line as csv and write to file pointer while ($row = $sql->fetch_assoc()) { $lineData = array($config_ticket_prefix . $row['ticket_number'], $row['ticket_priority'], $row['ticket_status_name'], $row['ticket_subject'], $row['ticket_created_at'], $row['ticket_resolved_at'], $row['ticket_closed_at']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/transaction.php b/agent/post/transaction.php index 01d1a5fb..bd7dae52 100644 --- a/agent/post/transaction.php +++ b/agent/post/transaction.php @@ -193,7 +193,7 @@ if (isset($_POST['export_transactions_csv'])) { //output each row of the data, format line as csv and write to file pointer while ($row = mysqli_fetch_assoc($sql)) { $lineData = array($row['transaction_date'], $row['transaction_type'], $row['transaction_description'], $row['transaction_other_account'], $row['transaction_reference'], $row['transaction_category'], $row['transaction_payment_method'], $row['transaction_amount'], $row['transaction_balance']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/trip.php b/agent/post/trip.php index fbf58f5c..13cbcdb9 100644 --- a/agent/post/trip.php +++ b/agent/post/trip.php @@ -139,7 +139,7 @@ if (isset($_POST['export_trips_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = mysqli_fetch_assoc($sql)){ $lineData = array($row['trip_date'], $row['trip_purpose'], $row['trip_source'], $row['trip_destination'], $row['trip_miles']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/agent/post/vendor.php b/agent/post/vendor.php index 0db34b59..4eb906d3 100644 --- a/agent/post/vendor.php +++ b/agent/post/vendor.php @@ -388,7 +388,7 @@ if (isset($_POST['export_vendors_csv'])) { //output each row of the data, format line as csv and write to file pointer while($row = $sql->fetch_assoc()) { $lineData = array($row['vendor_name'], $row['vendor_description'], $row['vendor_contact_name'], $row['vendor_phone'], $row['vendor_website'], $row['vendor_account_number'], $row['vendor_notes']); - fputcsv($f, $lineData, $delimiter, $enclosure, $escape); + fputcsv($f, array_map('escapeCsvFormula', $lineData), $delimiter, $enclosure, $escape); } //move back to beginning of file diff --git a/functions/sanitize.php b/functions/sanitize.php index 5cb4b618..409aa058 100644 --- a/functions/sanitize.php +++ b/functions/sanitize.php @@ -51,8 +51,7 @@ function cleanInput($input) { return $input; } -function toAlphanumeric($string) -{ +function toAlphanumeric($string) { // Gets rid of non-alphanumerics return preg_replace('/[^A-Za-z0-9_-]/', '', $string); } @@ -144,3 +143,29 @@ function checkFileUpload($file, $allowed_extensions) { // reading the whole file into memory (up to 500 MB) for no downstream use. return randomString(32) . '.' . $extension; } + +// Neutralize spreadsheet formula injection (CWE-1236) in a value bound for a +// generated CSV export. This is NOT CSV-structure escaping — fputcsv already +// quotes fields, doubles enclosures, and handles embedded newlines. Its only +// job is to stop a spreadsheet app (Excel / LibreOffice Calc / Google Sheets) +// from *evaluating* a syntactically valid cell as a formula on open. Prefixing +// a single quote makes the app treat the cell as literal text. +function escapeCsvFormula($value) { + if (!is_string($value) || $value === '') { + return $value; + } + + // Leave genuine numbers untouched so real data (e.g. negative amounts like + // -42.50) isn't corrupted. An actual formula string is never is_numeric(), + // so nothing dangerous slips through this early return. + if (is_numeric($value)) { + return $value; + } + + // Leading characters that trigger formula / legacy-DDE evaluation on open. + if (in_array($value[0], ['=', '+', '-', '@', "\t", "\r"], true)) { + return "'" . $value; + } + + return $value; +}