Files
opensourcepos/app/Models/Reports/Summary_discounts.php
jekkos 418580a52d Fix second-order SQL injection in currency_symbol config (#4390)
* Fix second-order SQL injection in currency_symbol config

The currency_symbol value was concatenated directly into SQL queries
without proper escaping, allowing SQL injection attacks via the
Summary Discounts report.

Changes:
- Use $this->db->escape() in Summary_discounts::getData() to properly
  escape the currency symbol value before concatenation
- Add htmlspecialchars() validation in Config::postSaveLocale() to
  sanitize the input at storage time
- Add unit tests to verify escaping of malicious inputs

Fixes SQL injection vulnerability described in bug report where
attackers with config permissions could inject arbitrary SQL through
the currency_symbol field.

* Update test to use CIUnitTestCase for consistency

Per code review feedback, updated test to extend CIUnitTestCase
instead of PHPUnit TestCase to maintain consistency with other
tests in the codebase.

---------

Co-authored-by: Ollama <ollama@steganos.dev>
2026-03-06 17:01:38 +01:00

50 lines
1.6 KiB
PHP

<?php
namespace App\Models\Reports;
use Config\OSPOS;
class Summary_discounts extends Summary_report
{
/**
* @return array[]
*/
protected function _get_data_columns(): array // TODO: Hungarian notation
{
return [
['discount' => lang('Reports.discount'), 'sorter' => 'number_sorter'],
['count' => lang('Reports.count')],
['total' => lang('Reports.total')]
];
}
/**
* @param array $inputs
* @return array
*/
public function getData(array $inputs): array
{
$config = config(OSPOS::class)->settings;
$builder = $this->db->table('sales_items AS sales_items');
if ($inputs['discount_type'] == FIXED) {
$currency_symbol = $this->db->escape($config['currency_symbol']);
$builder->select('SUM(sales_items.discount) AS total, MAX(CONCAT(' . $currency_symbol . ', sales_items.discount)) AS discount, count(*) AS count');
$builder->where('discount_type', FIXED);
} elseif ($inputs['discount_type'] == PERCENT) {
$builder->select('SUM(item_unit_price) * sales_items.discount / 100.0 AS total, MAX(CONCAT(sales_items.discount, "%")) AS discount, count(*) AS count');
$builder->where('discount_type', PERCENT);
}
$builder->where('discount >', 0);
$builder->groupBy('sales_items.discount');
$builder->orderBy('sales_items.discount');
$builder->join('sales AS sales', 'sales_items.sale_id = sales.sale_id', 'inner');
$this->_where($inputs, $builder); // TODO: Hungarian notation
return $builder->get()->getResultArray();
}
}