From cd7ebc0a5f996ba383ebd60da4f3d9c82d62e642 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 8 Oct 2026 16:17:46 -0400 Subject: [PATCH] Security: enforce per-user client restrictions on client, financial and bulk handlers and per-client reports Agents restricted from a client could still edit, archive, restore or delete it, run bulk actions on it (edits, tags, archive, email, tickets) and add credits. Every selected client is now checked before anything is written. Expense, trip and recurring expense edits now check the client the record already belongs to, not just the submitted one. Recurring expense delete, revenue edit/delete and task approval handlers gained the same check. Per-client reports and the dashboard payment/ticket lists now hide clients the agent is denied. Company-wide totals are unchanged. --- agent/dashboard.php | 2 + agent/post/client.php | 53 +++++++++++++++++++++ agent/post/credit.php | 3 ++ agent/post/expense.php | 10 +++- agent/post/recurring_expense.php | 10 ++++ agent/post/revenue.php | 11 +++++ agent/post/task.php | 8 ++++ agent/post/trip.php | 6 +++ agent/reports/client_ticket_time_detail.php | 2 + agent/reports/income_by_client.php | 5 +- agent/reports/outstanding_balances.php | 1 + agent/reports/recurring_by_client.php | 1 + agent/reports/sla_by_client.php | 2 +- agent/reports/ticket_by_client.php | 2 +- agent/reports/tickets_unbilled.php | 2 +- 15 files changed, 111 insertions(+), 7 deletions(-) diff --git a/agent/dashboard.php b/agent/dashboard.php index de218b403..51f64253a 100644 --- a/agent/dashboard.php +++ b/agent/dashboard.php @@ -123,6 +123,7 @@ if ($user_config_dashboard_financial_enable == 1) { SELECT client_name, invoice_number, invoice_prefix, payment_amount, payment_date FROM payments JOIN invoices ON payment_invoice_id = invoice_id JOIN clients ON invoice_client_id = client_id + WHERE 1=1 " . clientScopeSql('invoice_client_id') . " ORDER BY payment_id DESC LIMIT 5 "); @@ -607,6 +608,7 @@ if ($user_config_dashboard_technical_enable == 1) { LEFT JOIN contacts ON ticket_contact_id = contact_id WHERE ticket_assigned_to = $session_user_id AND ticket_closed_at IS NULL + " . clientScopeSql('ticket_client_id') . " ORDER BY ticket_number DESC "); ?> diff --git a/agent/post/client.php b/agent/post/client.php index d00a47738..c6f3fbbfd 100644 --- a/agent/post/client.php +++ b/agent/post/client.php @@ -140,6 +140,8 @@ if (isset($_POST['edit_client'])) { $client_id = intval($_POST['client_id']); + enforceClientAccess(); + // Update client mysqli_query($mysqli, "UPDATE clients SET client_name = '$name', client_type = '$type', client_website = '$website', client_referral = '$referral', client_rate = $rate, client_net_terms = $net_terms, client_tax_id_number = '$tax_id_number', client_lead = $lead, client_abbreviation = '$abbreviation', client_notes = '$notes' WHERE client_id = $client_id"); @@ -212,6 +214,8 @@ if (isset($_GET['archive_client'])) { $client_id = intval($_GET['archive_client']); + enforceClientAccess(); + // Archive client mysqli_query($mysqli, "UPDATE clients SET client_archived_at = NOW() WHERE client_id = $client_id"); @@ -242,6 +246,8 @@ if (isset($_GET['restore_client'])) { $client_id = intval($_GET['restore_client']); + enforceClientAccess(); + // Get Client Name $client_name = escapeSql(getFieldById('clients', $client_id, 'client_name')); @@ -263,6 +269,8 @@ if (isset($_GET['delete_client'])) { $client_id = intval($_GET['delete_client']); + enforceClientAccess(); + // Get Client Name $client_name = escapeSql(getFieldById('clients', $client_id, 'client_name')); @@ -761,6 +769,11 @@ if (isset($_POST['bulk_add_client_ticket'])) { // Create ticket for each selected asset if (isset($_POST['client_ids'])) { + // Check every selected client before creating any tickets + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + // Get a Asset Count $client_count = count($_POST['client_ids']); @@ -844,6 +857,11 @@ if (isset($_POST['bulk_edit_client_industry'])) { if (isset($_POST['client_ids'])) { + // Check every selected client before changing any of them + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + $count = count($_POST['client_ids']); foreach($_POST['client_ids'] as $client_id) { @@ -878,6 +896,11 @@ if (isset($_POST['bulk_edit_client_referral'])) { if (isset($_POST['client_ids'])) { + // Check every selected client before changing any of them + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + $count = count($_POST['client_ids']); foreach($_POST['client_ids'] as $client_id) { @@ -912,6 +935,11 @@ if (isset($_POST['bulk_edit_client_hourly_rate'])) { if (isset($_POST['client_ids'])) { + // Check every selected client before changing any of them + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + $count = count($_POST['client_ids']); foreach($_POST['client_ids'] as $client_id) { @@ -946,6 +974,11 @@ if (isset($_POST['bulk_edit_client_net_terms'])) { if (isset($_POST['client_ids'])) { + // Check every selected client before changing any of them + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + $count = count($_POST['client_ids']); foreach($_POST['client_ids'] as $client_id) { @@ -978,6 +1011,11 @@ if (isset($_POST['bulk_assign_client_tags'])) { if (isset($_POST['client_ids'])) { + // Check every selected client before changing any of them + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + $count = count($_POST['client_ids']); foreach($_POST['client_ids'] as $client_id) { @@ -1024,6 +1062,11 @@ if (isset($_POST['bulk_send_client_email']) && isset($_POST['client_ids'])) { $client_ids = array_map('intval', $_POST['client_ids']); $count = count($client_ids); + // Check every selected client before queuing anything + foreach ($client_ids as $client_id) { + enforceClientAccess($client_id); + } + // Email metadata $mail_from = escapeSql($_POST['mail_from']); $mail_from_name = escapeSql($_POST['mail_from_name']); @@ -1112,6 +1155,11 @@ if (isset($_POST['bulk_archive_clients'])) { if (isset($_POST['client_ids'])) { + // Check every selected client before changing any of them + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + $count = 0; foreach ($_POST['client_ids'] as $client_id) { @@ -1148,6 +1196,11 @@ if (isset($_POST['bulk_unarchive_clients'])) { if (isset($_POST['client_ids'])) { + // Check every selected client before changing any of them + foreach ($_POST['client_ids'] as $client_id) { + enforceClientAccess(intval($client_id)); + } + $count = count($_POST['client_ids']); foreach ($_POST['client_ids'] as $client_id) { diff --git a/agent/post/credit.php b/agent/post/credit.php index b2da02849..cfbb6711e 100644 --- a/agent/post/credit.php +++ b/agent/post/credit.php @@ -13,6 +13,9 @@ if (isset($_POST['add_credit'])) { enforceUserPermission('module_sales', 2); $client_id = intval($_POST['client']); + + enforceClientAccess(); + $amount = floatval($_POST['amount']); $type = escapeSql($_POST['type']); $expire = escapeSql($_POST['expire']); diff --git a/agent/post/expense.php b/agent/post/expense.php index d2f5c39a6..43d33b635 100644 --- a/agent/post/expense.php +++ b/agent/post/expense.php @@ -57,12 +57,18 @@ if (isset($_POST['edit_expense'])) { require_once 'expense_model.php'; + $expense_id = intval($_POST['expense_id']); + + // Check the client the expense belongs to now, not just the one submitted + $existing_client_id = intval(getFieldById('expenses', $expense_id, 'expense_client_id')); + if ($existing_client_id) { + enforceClientAccess($existing_client_id); + } + if ($client_id) { enforceClientAccess(); } - $expense_id = intval($_POST['expense_id']); - // Get old receipt $existing_file_name = escapeSql(getFieldById('expenses', $expense_id, 'expense_receipt')); diff --git a/agent/post/recurring_expense.php b/agent/post/recurring_expense.php index b683d3575..3f4032434 100644 --- a/agent/post/recurring_expense.php +++ b/agent/post/recurring_expense.php @@ -63,6 +63,12 @@ if (isset($_POST['edit_recurring_expense'])) { $description = escapeSql($_POST['description']); $reference = escapeSql($_POST['reference']); + // Check the client the recurring expense belongs to now, not just the one submitted + $existing_client_id = intval(getFieldById('recurring_expenses', $recurring_expense_id, 'recurring_expense_client_id')); + if ($existing_client_id) { + enforceClientAccess($existing_client_id); + } + if ($client_id) { enforceClientAccess(); } @@ -97,6 +103,10 @@ if (isset($_GET['delete_recurring_expense'])) { $recurring_expense_description = escapeSql($row['recurring_expense_description']); $client_id = intval($row['recurring_expense_client_id']); + if ($client_id) { + enforceClientAccess(); + } + mysqli_query($mysqli,"DELETE FROM recurring_expenses WHERE recurring_expense_id = $recurring_expense_id"); logAudit("Recurring Expense", "Delete", "$session_name deleted recurring expense $recurring_expense_description", $client_id); diff --git a/agent/post/revenue.php b/agent/post/revenue.php index 39d38aabb..f59114aa8 100644 --- a/agent/post/revenue.php +++ b/agent/post/revenue.php @@ -39,6 +39,12 @@ if (isset($_POST['edit_revenue'])) { enforceUserPermission('module_sales', 2); $revenue_id = intval($_POST['revenue_id']); + + $client_id = intval(getFieldById('revenues', $revenue_id, 'revenue_client_id')); + if ($client_id) { + enforceClientAccess(); + } + $date = escapeSql($_POST['date']); $amount = floatval($_POST['amount']); $account = intval($_POST['account']); @@ -65,6 +71,11 @@ if (isset($_GET['delete_revenue'])) { $revenue_id = intval($_GET['delete_revenue']); + $client_id = intval(getFieldById('revenues', $revenue_id, 'revenue_client_id')); + if ($client_id) { + enforceClientAccess(); + } + // Get Revenue Details $revenue_description = escapeSql(getFieldById('revenues', $revenue_id, 'revenue_description')); diff --git a/agent/post/task.php b/agent/post/task.php index 6c4035655..e68fa3453 100644 --- a/agent/post/task.php +++ b/agent/post/task.php @@ -364,6 +364,9 @@ if (isset($_GET['approve_ticket_task'])) { exit; } + $client_id = intval(getFieldById('tickets', $ticket_id, 'ticket_client_id')); + enforceClientAccess(); + // Validate approver (deny) if ($required_user > 0 && $required_user !== $session_user_id) { flashAlert("You cannot approve that task", 'error'); @@ -399,6 +402,11 @@ if (isset($_GET['delete_ticket_task_approver'])) { $approval_id = intval($_GET['delete_ticket_task_approver']); + $sql = mysqli_query($mysqli, "SELECT ticket_client_id FROM task_approvals LEFT JOIN tasks ON task_id = approval_task_id LEFT JOIN tickets ON ticket_id = task_ticket_id WHERE approval_id = $approval_id"); + $row = mysqli_fetch_assoc($sql); + $client_id = intval($row['ticket_client_id'] ?? 0); + enforceClientAccess(); + mysqli_query($mysqli, "DELETE FROM task_approvals WHERE approval_id = $approval_id"); logAudit("Task", "Delete", "$session_name deleted task approval request ($approval_id)", 0, 0); diff --git a/agent/post/trip.php b/agent/post/trip.php index 9e0cbc87b..35a669b2f 100644 --- a/agent/post/trip.php +++ b/agent/post/trip.php @@ -40,6 +40,12 @@ if (isset($_POST['edit_trip'])) { $trip_id = intval($_POST['trip_id']); + // Check the client the trip belongs to now, not just the one submitted + $existing_client_id = intval(getFieldById('trips', $trip_id, 'trip_client_id')); + if ($existing_client_id) { + enforceClientAccess($existing_client_id); + } + $client_id = intval($_POST['client_id']); if ($client_id) { diff --git a/agent/reports/client_ticket_time_detail.php b/agent/reports/client_ticket_time_detail.php index 8cbbc777b..7585e26f7 100644 --- a/agent/reports/client_ticket_time_detail.php +++ b/agent/reports/client_ticket_time_detail.php @@ -76,6 +76,7 @@ $billing_increment_minutes = (int) round($billing_increment_seconds / 60); // Ticket-level billable flag (same as your original report) $billable_sql = $billable_only ? " AND t.ticket_billable = 1 " : ""; +$client_scope_sql = clientScopeSql('c.client_id'); /** * Query returns ONLY replies that have time_worked and are within date range. @@ -108,6 +109,7 @@ $stmt = $mysqli->prepare(" WHERE c.client_archived_at IS NULL $billable_sql + $client_scope_sql ORDER BY c.client_name ASC, t.ticket_number ASC, diff --git a/agent/reports/income_by_client.php b/agent/reports/income_by_client.php index bad40193a..5d5c8a27f 100644 --- a/agent/reports/income_by_client.php +++ b/agent/reports/income_by_client.php @@ -48,9 +48,10 @@ $sql_payment_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(payment_date) A $sql_clients = "SELECT c.client_id, c.client_name, SUM(p.payment_amount) AS amount_paid FROM clients AS c JOIN invoices AS i ON c.client_id = i.invoice_client_id - JOIN payments AS p ON i.invoice_id = p.payment_invoice_id"; + JOIN payments AS p ON i.invoice_id = p.payment_invoice_id + WHERE 1=1" . clientScopeSql('c.client_id'); if ($year != 'all') { - $sql_clients .= " WHERE YEAR(p.payment_date) = $year"; + $sql_clients .= " AND YEAR(p.payment_date) = $year"; } $sql_clients .= " GROUP BY c.client_id HAVING amount_paid > 599 diff --git a/agent/reports/outstanding_balances.php b/agent/reports/outstanding_balances.php index 0a55a77a5..6af09b0e9 100644 --- a/agent/reports/outstanding_balances.php +++ b/agent/reports/outstanding_balances.php @@ -38,6 +38,7 @@ enforceUserPermission('module_financial'); GROUP BY payment_invoice_id) as payments ON invoices.invoice_id = payments.payment_invoice_id + WHERE 1=1 " . clientScopeSql('clients.client_id') . " GROUP BY clients.client_id, clients.client_name diff --git a/agent/reports/recurring_by_client.php b/agent/reports/recurring_by_client.php index e7204f233..3a5f76b44 100644 --- a/agent/reports/recurring_by_client.php +++ b/agent/reports/recurring_by_client.php @@ -11,6 +11,7 @@ $sql = mysqli_query($mysqli, " FROM clients LEFT JOIN recurring_invoices ON client_id = recurring_invoice_client_id WHERE recurring_invoice_status = 1 + " . clientScopeSql('clients.client_id') . " GROUP BY clients.client_id HAVING recurring_monthly_total > 0 ORDER BY recurring_monthly_total DESC diff --git a/agent/reports/sla_by_client.php b/agent/reports/sla_by_client.php index 4ea519d99..1b00305cf 100644 --- a/agent/reports/sla_by_client.php +++ b/agent/reports/sla_by_client.php @@ -25,7 +25,7 @@ if ($month) { $sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC"); -$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC"); +$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('client_id') . " ORDER BY client_name ASC"); // Clock time spent on resolved tickets, gathered for every client at once diff --git a/agent/reports/ticket_by_client.php b/agent/reports/ticket_by_client.php index dd7985872..03661e840 100644 --- a/agent/reports/ticket_by_client.php +++ b/agent/reports/ticket_by_client.php @@ -18,7 +18,7 @@ if (isset($_GET['month'])) { $sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC"); -$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC"); +$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('client_id') . " ORDER BY client_name ASC"); ?> diff --git a/agent/reports/tickets_unbilled.php b/agent/reports/tickets_unbilled.php index 0f1b9ba18..840ec0edd 100644 --- a/agent/reports/tickets_unbilled.php +++ b/agent/reports/tickets_unbilled.php @@ -12,7 +12,7 @@ if (isset($_GET['year'])) { $sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC"); -$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients ORDER BY client_name ASC"); +$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE 1=1 " . clientScopeSql('client_id') . " ORDER BY client_name ASC"); $rows = 0;