diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b7db7d..c909402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ --- develop --- +* security: Replace array_to_sql_or() and direct $selected_items concatenation with db_execute_prepared() and IN(?,?,?) placeholders in notify_lists.php bulk form actions +* security: Wrap all four bulk action blocks in db_begin_transaction()/db_commit_transaction(); rollback on db_execute_prepared() failure; break per-item loops immediately on error +* security: Move thold_template_update_thresholds() cascade after db_commit_transaction() so it does not participate in the transaction boundary +* security: Parameterize $graph_id in get_allowed_thresholds() and get_allowed_threshold_logs() using gl.id = ? placeholder; switch to db_fetch_assoc_prepared() and db_fetch_cell_prepared() +* security: Validate rfilter via FILTER_VALIDATE_IS_REGEX and escape with db_qstr() before use in RLIKE clauses +* security: Apply html_escape() to get_request_var('page') in thold.php and thold_graph.php hidden inputs; wrap AJAX filter URL params with encodeURIComponent() +* security: Apply sanitize_unserialize_selected_items() to selected_graphs_array in thold_webapi.php +* security: Cast drp_action allowlist keys to strings via array_map('strval', array_keys(...)) for correct strict in_array() comparison +* security: Add Pest v1 security test suite covering prepared statements, RLIKE injection, XSS escaping, unserialize hardening, PHP 7.4 compatibility, and smoke linting * issue#686: Applying a templated threshold to a graph via the wrench icon, creates a duplicate graph * issue#707: Excessive timeout for row caching prevents data from being updated timely * issue#710: Fixing Typo in thold_daemons.service File diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ea44404 --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "cacti/plugin-thold", + "description": "Thold Plugin for Cacti", + "type": "project", + "require-dev": { + "pestphp/pest": "^1.23" + }, + "autoload": { + "psr-4": {} + }, + "scripts": { + "test": "vendor/bin/pest tests/Security" + }, + "config": { + "vendor-dir": "vendor" + } +} diff --git a/notify_lists.php b/notify_lists.php index 016e01d..1c2b78b 100644 --- a/notify_lists.php +++ b/notify_lists.php @@ -147,6 +147,14 @@ function form_actions() { // ================= input validation ================= get_filter_request_var('drp_action'); + + $valid_actions = array_map('strval', array_keys($actions + $assoc_actions)); + + if (!in_array(get_request_var('drp_action'), $valid_actions, true)) { + raise_message(40); + header('Location: notify_lists.php'); + exit; + } // ==================================================== // if we are to save this form, instead of display it @@ -156,41 +164,60 @@ function form_actions() { if (isset_request_var('save_list')) { if ($selected_items != false) { if (get_request_var('drp_action') == '1') { // delete - db_execute('DELETE FROM plugin_notification_lists - WHERE ' . array_to_sql_or($selected_items, 'id')); + $placeholders = implode(',', array_fill(0, cacti_sizeof($selected_items), '?')); + + db_begin_transaction(); + + // Chain with && so the first failure short-circuits the remaining statements. + $ok = db_execute_prepared('DELETE FROM plugin_notification_lists + WHERE id IN (' . $placeholders . ')', + $selected_items) - db_execute('UPDATE host + && db_execute_prepared('UPDATE host SET thold_send_email = 0 WHERE thold_send_email = 2 - AND deleted="" - AND ' . array_to_sql_or($selected_items, 'thold_host_email')); + AND deleted = "" + AND thold_host_email IN (' . $placeholders . ')', + $selected_items) - db_execute('UPDATE host + && db_execute_prepared('UPDATE host SET thold_send_email = 1 WHERE thold_send_email = 3 - AND deleted="" - AND ' . array_to_sql_or($selected_items, 'thold_host_email')); + AND deleted = "" + AND thold_host_email IN (' . $placeholders . ')', + $selected_items) - db_execute('UPDATE host + && db_execute_prepared('UPDATE host SET thold_host_email = 0 - AND deleted="" - WHERE ' . array_to_sql_or($selected_items, 'thold_host_email')); + WHERE thold_host_email IN (' . $placeholders . ') + AND deleted = ""', + $selected_items) - db_execute('UPDATE thold_data + && db_execute_prepared('UPDATE thold_data SET notify_warning = 0 - WHERE ' . array_to_sql_or($selected_items, 'notify_warning')); + WHERE notify_warning IN (' . $placeholders . ')', + $selected_items) - db_execute('UPDATE thold_data + && db_execute_prepared('UPDATE thold_data SET notify_alert = 0 - WHERE ' . array_to_sql_or($selected_items, 'notify_alert')); + WHERE notify_alert IN (' . $placeholders . ')', + $selected_items) - db_execute('UPDATE thold_template + && db_execute_prepared('UPDATE thold_template SET notify_warning = 0 - WHERE ' . array_to_sql_or($selected_items, 'notify_warning')); + WHERE notify_warning IN (' . $placeholders . ')', + $selected_items) - db_execute('UPDATE thold_template + && db_execute_prepared('UPDATE thold_template SET notify_alert = 0 - WHERE ' . array_to_sql_or($selected_items, 'notify_alert')); + WHERE notify_alert IN (' . $placeholders . ')', + $selected_items); + + if ($ok) { + db_commit_transaction(); + } else { + db_rollback_transaction(); + } } elseif (get_request_var('drp_action') == '2') { // duplicate $i = 1; @@ -237,48 +264,60 @@ function form_actions() { if (isset_request_var('save_associate')) { if ($selected_items != false) { + get_filter_request_var('id'); get_filter_request_var('notification_action'); + get_filter_request_var('notification_warning_action'); + get_filter_request_var('notification_alert_action'); + + db_begin_transaction(); + + $ok = true; if (get_request_var('drp_action') == '1') { // associate - for ($i = 0; ($i < count($selected_items)); $i++) { + for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) { // set the notification list - db_execute('UPDATE host - SET thold_host_email=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i] . ' - AND deleted=""'); + $ok = db_execute_prepared('UPDATE host + SET thold_host_email = ? + WHERE id = ? + AND deleted = ""', + [get_request_var('id'), $selected_items[$i]]) && $ok; // set the global/list election - db_execute('UPDATE host - SET thold_send_email=' . get_request_var('notification_action') . ' - WHERE id=' . $selected_items[$i] . ' - AND deleted=""'); + $ok = db_execute_prepared('UPDATE host + SET thold_send_email = ? + WHERE id = ? + AND deleted = ""', + [get_request_var('notification_action'), $selected_items[$i]]) && $ok; if (get_request_var('notification_warning_action') > 0) { // clear other settings if (get_request_var('notification_warning_action') == 1) { // set the notification list - db_execute('UPDATE thold_data AS td + $ok = db_execute_prepared('UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - SET td.notify_warning=' . get_request_var('id') . ' - WHERE td.host_id=' . $selected_items[$i] . ' - AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)'); + SET td.notify_warning = ? + WHERE td.host_id = ? + AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)', + [get_request_var('id'), $selected_items[$i]]) && $ok; // clear other items - db_execute("UPDATE thold_data AS td + $ok = db_execute_prepared("UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - SET td.notify_warning_extra='' - WHERE td.host_id=" . $selected_items[$i] . ' - AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)'); + SET td.notify_warning_extra = '' + WHERE td.host_id = ? + AND (tt.notify_templated = \"\" OR tt.notify_templated IS NULL)", + [$selected_items[$i]]) && $ok; } else { // set the notification list - db_execute('UPDATE thold_data AS td + $ok = db_execute_prepared('UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - SET td.notify_warning=' . get_request_var('id') . ' - WHERE td.host_id=' . $selected_items[$i] . ' - AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)'); + SET td.notify_warning = ? + WHERE td.host_id = ? + AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)', + [get_request_var('id'), $selected_items[$i]]) && $ok; } } @@ -286,78 +325,100 @@ function form_actions() { // clear other settings if (get_request_var('notification_alert_action') == 1) { // set the notification list - db_execute('UPDATE thold_data AS td + $ok = db_execute_prepared('UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - SET td.notify_alert=' . get_request_var('id') . ' - WHERE td.host_id=' . $selected_items[$i] . ' - AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)'); + SET td.notify_alert = ? + WHERE td.host_id = ? + AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)', + [get_request_var('id'), $selected_items[$i]]) && $ok; // clear other items - db_execute("UPDATE thold_data AS td + $ok = db_execute_prepared("UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - SET td.notify_extra='' - WHERE host_id=" . $selected_items[$i] . ' - AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)'); + SET td.notify_extra = '' + WHERE host_id = ? + AND (tt.notify_templated = \"\" OR tt.notify_templated IS NULL)", + [$selected_items[$i]]) && $ok; // remove legacy contacts - db_execute('DELETE pttc + $ok = db_execute_prepared('DELETE pttc FROM plugin_thold_threshold_contact AS pttc INNER JOIN thold_data AS td ON pttc.thold_id = td.id LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - WHERE td.host_id=' . $selected_items[$i] . ' - AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)'); + WHERE td.host_id = ? + AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)', + [$selected_items[$i]]) && $ok; } else { // set the notification list - db_execute('UPDATE thold_data AS td + $ok = db_execute_prepared('UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - SET td.notify_alert=' . get_request_var('id') . ' - WHERE td.host_id=' . $selected_items[$i] . ' - AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)'); + SET td.notify_alert = ? + WHERE td.host_id = ? + AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)', + [get_request_var('id'), $selected_items[$i]]) && $ok; } } + + if (!$ok) { + break; + } } } elseif (get_request_var('drp_action') == '2') { // disassociate - for ($i = 0; ($i < count($selected_items)); $i++) { + for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) { // set the notification list - db_execute('UPDATE host - SET thold_host_email=0 - WHERE id=' . $selected_items[$i] . ' - AND deleted=""'); + $ok = db_execute_prepared('UPDATE host + SET thold_host_email = 0 + WHERE id = ? + AND deleted = ""', + [$selected_items[$i]]) && $ok; // set the global/list election - db_execute('UPDATE host - SET thold_send_email=' . get_request_var('notification_action') . ' - WHERE id=' . $selected_items[$i] . ' - AND deleted=""'); + $ok = db_execute_prepared('UPDATE host + SET thold_send_email = ? + WHERE id = ? + AND deleted = ""', + [get_request_var('notification_action'), $selected_items[$i]]) && $ok; if (get_request_var('notification_warning_action') > 0) { // set the notification list - db_execute('UPDATE thold_data AS td + $ok = db_execute_prepared('UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id SET td.notify_warning = 0 - WHERE td.host_id=' . $selected_items[$i] . ' + WHERE td.host_id = ? AND (tt.notify_templated = "" OR tt.notify_templated IS NULL) - AND td.notify_warning=' . get_request_var('id')); + AND td.notify_warning = ?', + [$selected_items[$i], get_request_var('id')]) && $ok; } if (get_request_var('notification_alert_action') > 0) { // set the notification list - db_execute('UPDATE thold_data AS td + $ok = db_execute_prepared('UPDATE thold_data AS td LEFT JOIN thold_template AS tt ON td.thold_template_id = tt.id - SET td.notify_alert=0 - WHERE td.host_id=' . $selected_items[$i] . ' + SET td.notify_alert = 0 + WHERE td.host_id = ? AND (tt.notify_templated = "" OR tt.notify_templated IS NULL) - AND td.notify_alert=' . get_request_var('id')); + AND td.notify_alert = ?', + [$selected_items[$i], get_request_var('id')]) && $ok; + } + + if (!$ok) { + break; } } } + + if ($ok) { + db_commit_transaction(); + } else { + db_rollback_transaction(); + } } header('Location: notify_lists.php?header=false&action=edit&tab=hosts&id=' . get_request_var('id')); @@ -366,27 +427,38 @@ function form_actions() { if (isset_request_var('save_templates')) { if ($selected_items != false) { + get_filter_request_var('id'); get_filter_request_var('notification_action'); + get_filter_request_var('notification_warning_action'); + get_filter_request_var('notification_alert_action'); + + db_begin_transaction(); + + $ok = true; + $update_template = []; if (get_request_var('drp_action') == '1') { // associate - for ($i = 0; ($i < count($selected_items)); $i++) { + for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) { if (get_request_var('notification_warning_action') > 0) { // clear other settings if (get_request_var('notification_warning_action') == 1) { // set the notification list - db_execute('UPDATE thold_template - SET notify_warning=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_template + SET notify_warning = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; // clear other items - db_execute("UPDATE thold_template - SET notify_warning_extra='' - WHERE id=" . $selected_items[$i]); + $ok = db_execute_prepared("UPDATE thold_template + SET notify_warning_extra = '' + WHERE id = ?", + [$selected_items[$i]]) && $ok; } else { // set the notification list - db_execute('UPDATE thold_template - SET notify_warning=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_template + SET notify_warning = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; } } @@ -394,47 +466,74 @@ function form_actions() { // clear other settings if (get_request_var('notification_alert_action') == 1) { // set the notification list - db_execute('UPDATE thold_template - SET notify_alert=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_template + SET notify_alert = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; // clear other items - db_execute("UPDATE thold_template - SET notify_extra='' - WHERE id=" . $selected_items[$i]); - - db_execute('DELETE FROM plugin_thold_template_contact - WHERE template_id=' . $selected_items[$i]); + $ok = db_execute_prepared("UPDATE thold_template + SET notify_extra = '' + WHERE id = ?", + [$selected_items[$i]]) && $ok; + + $ok = db_execute_prepared('DELETE FROM plugin_thold_template_contact + WHERE template_id = ?', + [$selected_items[$i]]) && $ok; } else { // set the notification list - db_execute('UPDATE thold_template - SET notify_alert=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_template + SET notify_alert = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; } } - thold_template_update_thresholds($selected_items[$i]); + $update_template[] = $selected_items[$i]; + + if (!$ok) { + break; + } } } elseif (get_request_var('drp_action') == '2') { // disassociate - for ($i = 0; ($i < count($selected_items)); $i++) { + for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) { if (get_request_var('notification_warning_action') > 0) { // set the notification list - db_execute('UPDATE thold_template - SET notify_warning=0 - WHERE id=' . $selected_items[$i] . ' - AND notify_warning=' . get_request_var('id')); + $ok = db_execute_prepared('UPDATE thold_template + SET notify_warning = 0 + WHERE id = ? + AND notify_warning = ?', + [$selected_items[$i], get_request_var('id')]) && $ok; } if (get_request_var('notification_alert_action') > 0) { // set the notification list - db_execute('UPDATE thold_template - SET notify_alert=0 - WHERE id=' . $selected_items[$i] . ' - AND notify_alert=' . get_request_var('id')); + $ok = db_execute_prepared('UPDATE thold_template + SET notify_alert = 0 + WHERE id = ? + AND notify_alert = ?', + [$selected_items[$i], get_request_var('id')]) && $ok; + } + + $update_template[] = $selected_items[$i]; + + if (!$ok) { + break; } + } + } + + if ($ok) { + db_commit_transaction(); - thold_template_update_thresholds($selected_items[$i]); + // Propagate template changes to threshold instances after the + // notification assignment is committed so this cascade does not + // participate in the transaction boundary. + foreach ($update_template as $template_id) { + thold_template_update_thresholds($template_id); } + } else { + db_rollback_transaction(); } } @@ -444,27 +543,37 @@ function form_actions() { if (isset_request_var('save_tholds')) { if ($selected_items != false) { + get_filter_request_var('id'); get_filter_request_var('notification_action'); + get_filter_request_var('notification_warning_action'); + get_filter_request_var('notification_alert_action'); + + db_begin_transaction(); + + $ok = true; if (get_request_var('drp_action') == '1') { // associate - for ($i = 0; ($i < count($selected_items)); $i++) { + for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) { if (get_request_var('notification_warning_action') > 0) { // clear other settings if (get_request_var('notification_warning_action') == 1) { // set the notification list - db_execute('UPDATE thold_data - SET notify_warning=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_data + SET notify_warning = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; // clear other items - db_execute("UPDATE thold_data - SET notify_warning_extra='' - WHERE id=" . $selected_items[$i]); + $ok = db_execute_prepared("UPDATE thold_data + SET notify_warning_extra = '' + WHERE id = ?", + [$selected_items[$i]]) && $ok; } else { // set the notification list - db_execute('UPDATE thold_data - SET notify_warning=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_data + SET notify_warning = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; } } @@ -472,43 +581,64 @@ function form_actions() { // clear other settings if (get_request_var('notification_alert_action') == 1) { // set the notification list - db_execute('UPDATE thold_data - SET notify_alert=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_data + SET notify_alert = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; // clear other items - db_execute("UPDATE thold_data - SET notify_extra='' - WHERE id=" . $selected_items[$i]); - - db_execute('DELETE FROM plugin_thold_threshold_contact WHERE thold_id=' . $selected_items[$i]); + $ok = db_execute_prepared("UPDATE thold_data + SET notify_extra = '' + WHERE id = ?", + [$selected_items[$i]]) && $ok; + + $ok = db_execute_prepared('DELETE FROM plugin_thold_threshold_contact + WHERE thold_id = ?', + [$selected_items[$i]]) && $ok; } else { // set the notification list - db_execute('UPDATE thold_data - SET notify_alert=' . get_request_var('id') . ' - WHERE id=' . $selected_items[$i]); + $ok = db_execute_prepared('UPDATE thold_data + SET notify_alert = ? + WHERE id = ?', + [get_request_var('id'), $selected_items[$i]]) && $ok; } } + + if (!$ok) { + break; + } } } elseif (get_request_var('drp_action') == '2') { // disassociate - for ($i = 0; ($i < count($selected_items)); $i++) { + for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) { if (get_request_var('notification_warning_action') > 0) { // set the notification list - db_execute('UPDATE thold_data - SET notify_warning=0 - WHERE id=' . $selected_items[$i] . ' - AND notify_warning=' . get_request_var('id')); + $ok = db_execute_prepared('UPDATE thold_data + SET notify_warning = 0 + WHERE id = ? + AND notify_warning = ?', + [$selected_items[$i], get_request_var('id')]) && $ok; } if (get_request_var('notification_alert_action') > 0) { // set the notification list - db_execute('UPDATE thold_data - SET notify_alert=0 - WHERE id=' . $selected_items[$i] . ' - AND notify_alert=' . get_request_var('id')); + $ok = db_execute_prepared('UPDATE thold_data + SET notify_alert = 0 + WHERE id = ? + AND notify_alert = ?', + [$selected_items[$i], get_request_var('id')]) && $ok; + } + + if (!$ok) { + break; } } } + + if ($ok) { + db_commit_transaction(); + } else { + db_rollback_transaction(); + } } header('Location: notify_lists.php?header=false&action=edit&tab=tholds&id=' . get_request_var('id')); @@ -1139,11 +1269,11 @@ function hosts($header_label) { function applyFilter() { strURL = '?header=false&action=edit&id=' - strURL += '&rows=' + $('#rows').val(); - strURL += '&host_template_id=' + $('#host_template_id').val(); - strURL += '&site_id=' + $('#site_id').val(); + strURL += '&rows=' + encodeURIComponent($('#rows').val()); + strURL += '&host_template_id=' + encodeURIComponent($('#host_template_id').val()); + strURL += '&site_id=' + encodeURIComponent($('#site_id').val()); strURL += '&associated=' + $('#associated').is(':checked'); - strURL += '&rfilter=' + base64_encode($('#rfilter').val()); + strURL += '&rfilter=' + encodeURIComponent(base64_encode($('#rfilter').val())); loadPageNoHeader(strURL); } @@ -1399,7 +1529,9 @@ function tholds($header_label) { } if (strlen(get_request_var('rfilter'))) { - $sql_where .= (!strlen($sql_where) ? '' : ' AND ') . "td.name_cache RLIKE '" . get_request_var('rfilter') . "'"; + // rfilter is pre-validated as a legal PHP regex by FILTER_VALIDATE_IS_REGEX in the + // request validation array; db_qstr() SQL-escapes the already-validated value. + $sql_where .= (!strlen($sql_where) ? '' : ' AND ') . 'td.name_cache RLIKE ' . db_qstr(get_request_var('rfilter')); } if ($statefilter != '') { @@ -1509,11 +1641,11 @@ function tholds($header_label) { function applyFilter() { strURL = 'notify_lists.php?header=false&action=edit&tab=tholds&id=' strURL += '&associated=' + $('#associated').is(':checked'); - strURL += '&state=' + $('#state').val(); - strURL += '&site_id=' + $('#site_id').val(); - strURL += '&rows=' + $('#rows').val(); - strURL += '&template=' + $('#template').val(); - strURL += '&rfilter=' + base64_encode($('#rfilter').val()); + strURL += '&state=' + encodeURIComponent($('#state').val()); + strURL += '&site_id=' + encodeURIComponent($('#site_id').val()); + strURL += '&rows=' + encodeURIComponent($('#rows').val()); + strURL += '&template=' + encodeURIComponent($('#template').val()); + strURL += '&rfilter=' + encodeURIComponent(base64_encode($('#rfilter').val())); loadPageNoHeader(strURL); } @@ -1739,7 +1871,9 @@ function templates($header_label) { } if (strlen(get_request_var('rfilter'))) { - $sql_where .= (!strlen($sql_where) ? 'WHERE ' : ' AND ') . "thold_template.name RLIKE '" . get_request_var('rfilter') . "'"; + // rfilter is pre-validated as a legal PHP regex by FILTER_VALIDATE_IS_REGEX in the + // request validation array; db_qstr() SQL-escapes the already-validated value. + $sql_where .= (!strlen($sql_where) ? 'WHERE ' : ' AND ') . 'thold_template.name RLIKE ' . db_qstr(get_request_var('rfilter')); } $sql = "SELECT * @@ -1798,8 +1932,8 @@ function templates($header_label) { function applyFilter() { strURL = 'notify_lists.php?header=false&action=edit&tab=templates&id=' strURL += '&associated=' + $('#associated').is(':checked'); - strURL += '&rows=' + $('#rows').val(); - strURL += '&rfilter=' + base64_encode($('#rfilter').val()); + strURL += '&rows=' + encodeURIComponent($('#rows').val()); + strURL += '&rfilter=' + encodeURIComponent(base64_encode($('#rfilter').val())); loadPageNoHeader(strURL); } @@ -2117,8 +2251,8 @@ function lists() { function applyFilter() { strURL = 'notify_lists.php?header=false'; - strURL += '&rows=' + $('#rows').val(); - strURL += '&rfilter=' + base64_encode($('#rfilter').val()); + strURL += '&rows=' + encodeURIComponent($('#rows').val()); + strURL += '&rfilter=' + encodeURIComponent(base64_encode($('#rfilter').val())); loadPageNoHeader(strURL); } @@ -2143,10 +2277,12 @@ function clearFilter() { // form the 'where' clause for our main sql query if (strlen(get_request_var('rfilter'))) { - $sql_where = "WHERE ( - name RLIKE '" . get_request_var('rfilter') . "' - OR description RLIKE '" . get_request_var('rfilter') . "' - OR emails RLIKE '" . get_request_var('rfilter') . "')"; + // rfilter is pre-validated as a legal PHP regex by FILTER_VALIDATE_IS_REGEX in the + // request validation array; db_qstr() SQL-escapes the already-validated value. + $sql_where = 'WHERE ( + name RLIKE ' . db_qstr(get_request_var('rfilter')) . ' + OR description RLIKE ' . db_qstr(get_request_var('rfilter')) . ' + OR emails RLIKE ' . db_qstr(get_request_var('rfilter')) . ')'; } else { $sql_where = ''; } diff --git a/notify_queue.php b/notify_queue.php index 12f2464..8117483 100644 --- a/notify_queue.php +++ b/notify_queue.php @@ -318,10 +318,10 @@ function notify_queue() { function applyFilter() { strURL = 'notify_queue.php?header=false'; - strURL += '&filter='+$('#filter').val(); - strURL += '&rows='+$('#rows').val(); - strURL += '&processed='+$('#processed').val(); - strURL += '&topic='+$('#topic').val(); + strURL += '&filter='+encodeURIComponent($('#filter').val()); + strURL += '&rows='+encodeURIComponent($('#rows').val()); + strURL += '&processed='+encodeURIComponent($('#processed').val()); + strURL += '&topic='+encodeURIComponent($('#topic').val()); loadPageNoHeader(strURL); } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..cfde204 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,15 @@ + + + + + tests/Security + + + + + + + diff --git a/setup.php b/setup.php index b9aedfc..4529801 100644 --- a/setup.php +++ b/setup.php @@ -691,7 +691,7 @@ function thold_device_action_execute($action) { $selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items')); if ($selected_items != false) { - for ($i = 0; ($i < count($selected_items)); $i++) { + for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) { autocreate($selected_items[$i]); } } diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..e6bf268 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,10 @@ +not->toBeFalse("Failed to resolve target file path: {$relativeFile}"); + + $contents = file_get_contents($path); + expect($contents)->not->toBeFalse("Failed to read target file: {$relativeFile}"); + + return $contents; +} + +it('does not use str_contains (PHP 8.0)', function () { + foreach (thold_security_compatibility_files() as $relativeFile) { + $contents = thold_security_read_file($relativeFile); + + expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0, + "{$relativeFile} uses str_contains() which requires PHP 8.0" + ); + } +}); + +it('does not use str_starts_with (PHP 8.0)', function () { + foreach (thold_security_compatibility_files() as $relativeFile) { + $contents = thold_security_read_file($relativeFile); + + expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0, + "{$relativeFile} uses str_starts_with() which requires PHP 8.0" + ); + } +}); + +it('does not use str_ends_with (PHP 8.0)', function () { + foreach (thold_security_compatibility_files() as $relativeFile) { + $contents = thold_security_read_file($relativeFile); + + expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0, + "{$relativeFile} uses str_ends_with() which requires PHP 8.0" + ); + } +}); + +it('does not use nullsafe operator (PHP 8.0)', function () { + foreach (thold_security_compatibility_files() as $relativeFile) { + $contents = thold_security_read_file($relativeFile); + + expect(preg_match('/\?->/', $contents))->toBe(0, + "{$relativeFile} uses nullsafe operator which requires PHP 8.0" + ); + } +}); diff --git a/tests/Security/PreparedStatementConsistencyTest.php b/tests/Security/PreparedStatementConsistencyTest.php new file mode 100644 index 0000000..441b8a0 --- /dev/null +++ b/tests/Security/PreparedStatementConsistencyTest.php @@ -0,0 +1,50 @@ +not->toBeFalse("Failed to resolve target file path: {$relativeFile}"); + + $contents = file_get_contents($path); + expect($contents)->not->toBeFalse("Failed to read target file: {$relativeFile}"); + + $lines = explode("\n", $contents); + + foreach ($lines as $lineNumber => $line) { + $trimmed = ltrim($line); + + if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0 || strpos($trimmed, '#') === 0) { + continue; + } + + $hasInterpolatedRawCall = preg_match($rawInterpolatedPattern, $line) === 1; + $hasPreparedCall = preg_match($preparedPattern, $line) === 1; + + expect($hasInterpolatedRawCall && !$hasPreparedCall)->toBeFalse( + sprintf('File %s contains an interpolated raw db_* call at line %d', $relativeFile, $lineNumber + 1) + ); + } + } +}); diff --git a/tests/Security/PreparedStatementTest.php b/tests/Security/PreparedStatementTest.php new file mode 100644 index 0000000..ff7db9c --- /dev/null +++ b/tests/Security/PreparedStatementTest.php @@ -0,0 +1,118 @@ +not->toContain('array_to_sql_or($selected_items'); +}); + +it('notify_lists.php delete action uses db_execute_prepared with IN placeholders', function () use ($notify_src) { + expect($notify_src)->toContain('db_execute_prepared(\'DELETE FROM plugin_notification_lists'); + expect($notify_src)->toContain('$placeholders'); +}); + +it('notify_lists.php associate action uses db_execute_prepared for host updates', function () use ($notify_src) { + expect($notify_src)->toContain('db_execute_prepared(\'UPDATE host'); + expect($notify_src)->toContain('SET thold_host_email = ?'); +}); + +it('notify_lists.php does not concatenate selected_items[$i] directly into SQL strings', function () use ($notify_src) { + expect(preg_match("/WHERE id='\s*\\.\\s*\\\$selected_items/", $notify_src))->toBe(0); + expect(preg_match('/WHERE id=\' \. \$selected_items/', $notify_src))->toBe(0); + expect(preg_match('/WHERE id=' . "'" . ' \. \$selected_items/', $notify_src))->toBe(0); +}); + +it('notify_lists.php uses cacti_sizeof instead of count for selected_items loops', function () use ($notify_src) { + expect($notify_src)->toContain('cacti_sizeof($selected_items)'); + expect(preg_match('/count\(\$selected_items\)/', $notify_src))->toBe(0); +}); + +it('thold_functions.php get_allowed_thresholds uses db_fetch_assoc_prepared', function () use ($funcs_src) { + expect($funcs_src)->toContain('db_fetch_assoc_prepared($tholds_sql, $sql_params)'); +}); + +it('thold_functions.php get_allowed_thresholds uses db_fetch_cell_prepared for row count', function () use ($funcs_src) { + expect($funcs_src)->toContain('db_fetch_cell_prepared($sql, $sql_params)'); +}); + +it('thold_functions.php get_allowed_thresholds does not interpolate graph_id directly', function () use ($funcs_src) { + expect($funcs_src)->not->toContain('gl.id=$graph_id'); + expect($funcs_src)->not->toContain('gl.id = $graph_id'); +}); + +it('thold_functions.php get_allowed_threshold_logs uses db_fetch_assoc_prepared', function () use ($funcs_src) { + expect($funcs_src)->toContain('db_fetch_assoc_prepared("SELECT'); +}); + +it('notify_lists.php drp_action guard converts keys to strings before strict comparison', function () use ($notify_src) { + // array_keys() returns int keys; POST values are strings; strval() cast allows strict in_array() + expect($notify_src)->toContain("array_map('strval', array_keys(\$actions + \$assoc_actions))"); + expect($notify_src)->toContain("in_array(get_request_var('drp_action'), \$valid_actions, true)"); +}); + +it('notify_lists.php bulk write actions are wrapped in transactions', function () use ($notify_src) { + // All four bulk action blocks must begin a transaction. + $beginCount = substr_count($notify_src, 'db_begin_transaction()'); + expect($beginCount)->toBe(4); +}); + +it('notify_lists.php bulk write actions commit on success', function () use ($notify_src) { + $commitCount = substr_count($notify_src, 'db_commit_transaction()'); + expect($commitCount)->toBe(4); +}); + +it('notify_lists.php bulk write actions rollback on failure', function () use ($notify_src) { + // Each transaction block must have a matching rollback path for when db_execute_prepared returns false. + $rollbackCount = substr_count($notify_src, 'db_rollback_transaction()'); + expect($rollbackCount)->toBe(4); +}); + +it('notify_lists.php bulk write actions track $ok flag for all db_execute_prepared calls', function () use ($notify_src) { + // Every db_execute_prepared result must be ANDed into $ok so partial failure triggers rollback. + expect($notify_src)->toContain('$ok = true'); + expect(preg_match('/\$ok\s*=\s*db_execute_prepared/', $notify_src))->toBe(1); + // [^)]+ stops at the first ) inside multi-argument calls; use substr_count instead + expect(substr_count($notify_src, ') && $ok'))->toBeGreaterThanOrEqual(6); +}); + +it('notify_lists.php per-item loops break immediately on first failure', function () use ($notify_src) { + // Each loop must break as soon as $ok is false rather than continuing to issue DB calls. + // associate (2 loops) + save_templates (2 loops) + save_tholds (2 loops) = 6 break guards. + // The flat delete sequence has no loop and therefore no break guard. + $breakCount = substr_count($notify_src, 'if (!$ok) {'); + expect($breakCount)->toBeGreaterThanOrEqual(6); + expect($notify_src)->toContain('if (!$ok) {'); + expect($notify_src)->toContain('break;'); +}); + +it('notify_lists.php save_templates calls thold_template_update_thresholds after commit only', function () use ($notify_src) { + // thold_template_update_thresholds must not run inside the transaction boundary + // (it should be called after db_commit_transaction() in the on-success path). + $commitPos = strpos($notify_src, 'db_commit_transaction();' . "\n\n\t\t\t\t\t// Propagate"); + expect($commitPos)->not->toBeFalse(); + // The function must not appear between db_begin_transaction and db_commit_transaction in this block. + $beginPos = strrpos(substr($notify_src, 0, $commitPos), 'db_begin_transaction()'); + $between = substr($notify_src, $beginPos, $commitPos - $beginPos); + expect($between)->not->toContain('thold_template_update_thresholds'); +}); diff --git a/tests/Security/RlikeInjectionTest.php b/tests/Security/RlikeInjectionTest.php new file mode 100644 index 0000000..8ba40fe --- /dev/null +++ b/tests/Security/RlikeInjectionTest.php @@ -0,0 +1,79 @@ +not->toContain($vulnerable_dq); + expect($src)->not->toContain($vulnerable_sq); +}); + +it('thold_graph.php RLIKE patterns use db_qstr escaping', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../thold_graph.php')); + expect($src)->toContain("RLIKE ' . db_qstr(get_request_var('rfilter'))"); +}); + +it('thold.php has no raw rfilter concatenated into RLIKE', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../thold.php')); + $vulnerable_dq = <<<'EOT' +RLIKE '" . get_request_var('rfilter') . "' +EOT; + expect($src)->not->toContain($vulnerable_dq); +}); + +it('thold.php RLIKE pattern uses db_qstr escaping', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../thold.php')); + expect($src)->toContain("RLIKE ' . db_qstr(get_request_var('rfilter'))"); +}); + +it('notify_lists.php has no raw rfilter concatenated into RLIKE', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../notify_lists.php')); + $vulnerable_dq = <<<'EOT' +RLIKE '" . get_request_var('rfilter') . "' +EOT; + expect($src)->not->toContain($vulnerable_dq); +}); + +it('notify_lists.php RLIKE patterns use db_qstr escaping', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../notify_lists.php')); + expect($src)->toContain("RLIKE ' . db_qstr(get_request_var('rfilter'))"); +}); + +it('db_qstr wraps value in single-quoted escaped string', function () { + expect(db_qstr("O'Brien"))->toBe("'O''Brien'"); + expect(db_qstr('normal'))->toBe("'normal'"); + expect(db_qstr("1' OR '1'='1"))->toBe("'1'' OR ''1''=''1'"); +}); + +it('rfilter is validated as a PHP regex before reaching any RLIKE clause', function () { + // thold_graph.php, thold.php, and notify_lists.php all declare rfilter with + // FILTER_VALIDATE_IS_REGEX in their request validation arrays. This means + // get_filter_request_var() rejects malformed or catastrophic patterns before + // any SQL is constructed, mitigating ReDoS at the MySQL RLIKE engine. + foreach (['thold_graph.php', 'thold.php', 'notify_lists.php'] as $file) { + $src = file_get_contents(realpath(__DIR__ . '/../../' . $file)); + expect($src)->toContain('FILTER_VALIDATE_IS_REGEX'); + } +}); diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php new file mode 100644 index 0000000..b3e0cd4 --- /dev/null +++ b/tests/Security/SetupStructureTest.php @@ -0,0 +1,38 @@ +toContain('function plugin_thold_install'); +}); + +it('setup.php defines plugin_thold_uninstall', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../setup.php')); + expect($src)->toContain('function plugin_thold_uninstall'); +}); + +it('setup.php defines plugin_thold_version', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../setup.php')); + expect($src)->toContain('function plugin_thold_version'); +}); + +it('setup.php defines plugin_thold_check_config', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../setup.php')); + expect($src)->toContain('function plugin_thold_check_config'); +}); + +it('setup.php reads plugin metadata from INFO file', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../setup.php')); + expect($src)->toContain('parse_ini_file'); +}); + +it('setup.php registers poller_output hook', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../setup.php')); + expect($src)->toContain("'poller_output'"); +}); diff --git a/tests/Security/TriggerCmdRegressionTest.php b/tests/Security/TriggerCmdRegressionTest.php new file mode 100644 index 0000000..96c0990 --- /dev/null +++ b/tests/Security/TriggerCmdRegressionTest.php @@ -0,0 +1,36 @@ +toContain("thold_data['trigger_cmd_low']"); +}); + +it('thold_functions.php references trigger_cmd_norm for norm-restoration environ setup', function () use ($funcs) { + expect($funcs)->toContain("thold_data['trigger_cmd_norm']"); +}); + +it('thold_set_environ in low-breach branch uses trigger_cmd_low', function () use ($funcs) { + expect(preg_match('/thold_set_environ\s*\(\s*\$thold_data\[.trigger_cmd_low.\]/', $funcs))->toBe(1); +}); + +it('thold_set_environ in norm-restoration branch uses trigger_cmd_norm', function () use ($funcs) { + expect(preg_match('/thold_set_environ\s*\(\s*\$thold_data\[.trigger_cmd_norm.\]/', $funcs))->toBe(1); +}); diff --git a/tests/Security/UnserializeHardeningTest.php b/tests/Security/UnserializeHardeningTest.php new file mode 100644 index 0000000..9e6cc19 --- /dev/null +++ b/tests/Security/UnserializeHardeningTest.php @@ -0,0 +1,45 @@ +not->toContain("stripslashes(get_nfilter_request_var('selected_graphs_array')"); +}); + +it('thold_webapi.php uses sanitize_unserialize_selected_items for selected_graphs_array', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../thold_webapi.php')); + expect($src)->toContain("sanitize_unserialize_selected_items(get_nfilter_request_var('selected_graphs_array')"); +}); + +it('sanitize_unserialize_selected_items rejects serialized objects', function () { + $payload = serialize(new stdClass()); + expect(sanitize_unserialize_selected_items($payload))->toBeFalse(); +}); + +it('sanitize_unserialize_selected_items accepts array of integers', function () { + $payload = serialize([1, 2, 3]); + $result = sanitize_unserialize_selected_items($payload); + expect($result)->toBeArray(); + expect($result)->toEqual([1, 2, 3]); +}); + +it('sanitize_unserialize_selected_items rejects arrays containing non-numeric values', function () { + $payload = serialize(['id' => '1; DROP TABLE thold_data']); + expect(sanitize_unserialize_selected_items($payload))->toBeFalse(); +}); diff --git a/tests/Security/XssEscapingTest.php b/tests/Security/XssEscapingTest.php new file mode 100644 index 0000000..57c3e12 --- /dev/null +++ b/tests/Security/XssEscapingTest.php @@ -0,0 +1,59 @@ +not->toContain("value=''"); + expect($src)->toContain("html_escape(get_request_var('page'))"); +}); + +it('html_escape converts angle brackets to entities', function () { + expect(html_escape(''))->toBe('<script>alert(1)</script>'); +}); + +it('html_escape converts double quotes to entities', function () { + expect(html_escape('"quoted"'))->toBe('"quoted"'); +}); + +it('html_escape converts single quotes to entities', function () { + // ENT_QUOTES|ENT_HTML5 encodes single quotes as ' (HTML5 named entity) + expect(html_escape("O'Brien"))->toBe('O'Brien'); +}); + +it('thold.php AJAX filter uses encodeURIComponent for URL params', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../thold.php')); + // rfilter is base64-encoded then URI-encoded; other params are URI-encoded directly + expect($src)->toContain("encodeURIComponent(base64_encode($('#rfilter').val()))"); + expect($src)->toContain("encodeURIComponent($('#rows').val())"); +}); + +it('thold_graph.php AJAX filter uses encodeURIComponent for URL params', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../thold_graph.php')); + expect($src)->toContain('encodeURIComponent'); +}); + +it('notify_lists.php AJAX filter uses encodeURIComponent for URL params', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../notify_lists.php')); + expect($src)->toContain('encodeURIComponent'); +}); + +it('thold.php hidden page input uses html_escape', function () { + $src = file_get_contents(realpath(__DIR__ . '/../../thold.php')); + expect($src)->toContain("html_escape(get_filter_request_var('page'))"); + expect($src)->not->toContain("print get_filter_request_var('page')"); +}); diff --git a/tests/Smoke/PhpSyntaxTest.php b/tests/Smoke/PhpSyntaxTest.php new file mode 100644 index 0000000..1fd8911 --- /dev/null +++ b/tests/Smoke/PhpSyntaxTest.php @@ -0,0 +1,59 @@ +getExtension() !== 'php') { + continue; + } + + $rel = ltrim(str_replace($root, '', $file->getPathname()), DIRECTORY_SEPARATOR); + + if (strncmp($rel, 'vendor' . DIRECTORY_SEPARATOR, strlen('vendor' . DIRECTORY_SEPARATOR)) === 0) { + continue; + } + + if (strncmp($rel, 'tests' . DIRECTORY_SEPARATOR, strlen('tests' . DIRECTORY_SEPARATOR)) === 0) { + continue; + } + + $files[] = $file->getPathname(); +} + +sort($files); + +it('all plugin PHP files parse without errors', function () use ($phpBin, $files) { + $failures = []; + + foreach ($files as $file) { + // bare escapeshellarg(): cacti_escapeshellarg() requires the Cacti bootstrap; $file is a local path, not user input + exec(escapeshellarg($phpBin) . ' -l ' . escapeshellarg($file) . ' 2>&1', $output, $code); // nosemgrep: php.lang.security.exec-use.exec-use -- lint check only; $file is a glob-returned server-local path with no user input + if ($code !== 0) { + $failures[] = basename($file) . ': ' . implode(' ', $output); + } + $output = []; + } + + expect($failures)->toBe([]); +}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..29e4279 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,187 @@ + '/var/www/html/cacti', + 'url_path' => '/cacti/', + 'cacti_version' => '1.2.999', +]; + +if (!function_exists('db_execute')) { + function db_execute($sql) { + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_execute', 'sql' => $sql, 'params' => []]; + + return true; + } +} + +if (!function_exists('db_execute_prepared')) { + function db_execute_prepared($sql, $params = []) { + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params]; + + return true; + } +} + +if (!function_exists('db_fetch_assoc')) { + function db_fetch_assoc($sql) { + return []; + } +} + +if (!function_exists('db_fetch_assoc_prepared')) { + function db_fetch_assoc_prepared($sql, $params = []) { + return []; + } +} + +if (!function_exists('db_fetch_row')) { + function db_fetch_row($sql) { + return []; + } +} + +if (!function_exists('db_fetch_row_prepared')) { + function db_fetch_row_prepared($sql, $params = []) { + return []; + } +} + +if (!function_exists('db_fetch_cell')) { + function db_fetch_cell($sql) { + return ''; + } +} + +if (!function_exists('db_fetch_cell_prepared')) { + function db_fetch_cell_prepared($sql, $params = []) { + return ''; + } +} + +if (!function_exists('db_qstr')) { + function db_qstr($string) { + return "'" . str_replace("'", "''", $string) . "'"; + } +} + +if (!function_exists('db_begin_transaction')) { + function db_begin_transaction() { + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_begin_transaction', 'sql' => '', 'params' => []]; + + return true; + } +} + +if (!function_exists('db_commit_transaction')) { + function db_commit_transaction() { + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_commit_transaction', 'sql' => '', 'params' => []]; + + return true; + } +} + +if (!function_exists('db_rollback_transaction')) { + function db_rollback_transaction() { + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_rollback_transaction', 'sql' => '', 'params' => []]; + + return true; + } +} + +if (!function_exists('html_escape')) { + function html_escape($string) { + return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } +} + +// KEEP IN SYNC with Cacti core lib/functions.php sanitize_unserialize_selected_items() +if (!function_exists('sanitize_unserialize_selected_items')) { + function sanitize_unserialize_selected_items($items) { + if (empty($items)) { + return false; + } + + $data = unserialize($items, ['allowed_classes' => false]); // nosemgrep: php.lang.security.unserialize-use.unserialize-use -- test stub mirrors sanitize_unserialize_selected_items; allowed_classes:false blocks object injection + + if (!is_array($data)) { + return false; + } + + foreach ($data as $key => $value) { + if (!is_numeric($value)) { + return false; + } + } + + return $data; + } +} + +if (!function_exists('read_config_option')) { + function read_config_option($name, $force = false) { + return ''; + } +} + +if (!function_exists('set_config_option')) { + function set_config_option($name, $value) { + } +} + +if (!function_exists('__')) { + function __($text, $domain = '') { + return $text; + } +} + +if (!function_exists('__esc')) { + function __esc($text, $domain = '') { + return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } +} + +if (!function_exists('cacti_log')) { + function cacti_log($message, $also_print = false, $log_type = '', $level = 0) { + } +} + +if (!function_exists('cacti_sizeof')) { + function cacti_sizeof($array) { + return is_array($array) ? count($array) : 0; + } +} + +if (!function_exists('get_request_var')) { + function get_request_var($name) { + return ''; + } +} + +if (!function_exists('get_nfilter_request_var')) { + function get_nfilter_request_var($name) { + return ''; + } +} + +if (!function_exists('get_filter_request_var')) { + function get_filter_request_var($name) { + return ''; + } +} + +if (!defined('CACTI_PATH_BASE')) { + define('CACTI_PATH_BASE', '/var/www/html/cacti'); +} diff --git a/thold.php b/thold.php index 0bf86f4..dc40024 100644 --- a/thold.php +++ b/thold.php @@ -614,7 +614,7 @@ function list_tholds() { } if (get_request_var('rfilter') != '') { - $sql_where .= ($sql_where == '' ? '(' : ' AND ') . " td.name_cache RLIKE '" . get_request_var('rfilter') . "'"; + $sql_where .= ($sql_where == '' ? '(' : ' AND ') . ' td.name_cache RLIKE ' . db_qstr(get_request_var('rfilter')); } if ($statefilter != '') { @@ -763,18 +763,18 @@ function list_tholds() { - '> + '>