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() {
- '>
+ '>